mirror of
https://github.com/esphome/esphome.git
synced 2026-02-11 02:02:09 +00:00
Compare commits
115 Commits
text_fix_h
...
action_cal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b22a8b00bf | ||
|
|
297f05d600 | ||
|
|
54fc10714d | ||
|
|
889886909b | ||
|
|
655e2b43cb | ||
|
|
81e639a6ba | ||
|
|
f9ffd134df | ||
|
|
c50bf45496 | ||
|
|
9f9341a700 | ||
|
|
71d532a349 | ||
|
|
61a89a97d7 | ||
|
|
0c3433d056 | ||
|
|
7e1cda8f9f | ||
|
|
7f0e4eaa84 | ||
|
|
8cccfa5369 | ||
|
|
7ea6bcef88 | ||
|
|
353daa97d0 | ||
|
|
6c68ebe86e | ||
|
|
29cef3bc5d | ||
|
|
83eebdf15d | ||
|
|
595217786c | ||
|
|
912f94d1e8 | ||
|
|
ea8ae2ae60 | ||
|
|
e1aac7601d | ||
|
|
f1b11b1855 | ||
|
|
23f9f70b71 | ||
|
|
eeeae53f76 | ||
|
|
45c0796e40 | ||
|
|
38e2e4a56d | ||
|
|
52132ea3bc | ||
|
|
ace3ff2170 | ||
|
|
26e90b4ca6 | ||
|
|
684790c2ab | ||
|
|
6a3737bac3 | ||
|
|
723ca57617 | ||
|
|
909bd1074a | ||
|
|
68064dc974 | ||
|
|
742d724e65 | ||
|
|
5ae46a4369 | ||
|
|
a1395af763 | ||
|
|
6222fae907 | ||
|
|
e34532f283 | ||
|
|
f2eb61a767 | ||
|
|
5725a4840e | ||
|
|
de82f96ccb | ||
|
|
6c981d8b71 | ||
|
|
c03faf2d9a | ||
|
|
da7680f7d9 | ||
|
|
cea2878b55 | ||
|
|
e0ff7fdaa1 | ||
|
|
3c9b300c46 | ||
|
|
32f90b2855 | ||
|
|
2fb7c0d453 | ||
|
|
7935fba4b1 | ||
|
|
ab32b93928 | ||
|
|
3d54ccac65 | ||
|
|
c40f44f4bd | ||
|
|
62cb08c3dc | ||
|
|
7576e032f8 | ||
|
|
cd43b4114e | ||
|
|
2c165e4817 | ||
|
|
5afe4b7b12 | ||
|
|
dcb8c994cc | ||
|
|
012a1e2afd | ||
|
|
d4969f581a | ||
|
|
40f108116b | ||
|
|
52459d1bc7 | ||
|
|
325c938074 | ||
|
|
423a617b15 | ||
|
|
eb5c4f34e2 | ||
|
|
c9ab4ca018 | ||
|
|
da0b01f4d0 | ||
|
|
e301b8d0e0 | ||
|
|
738678e87b | ||
|
|
0ce3ac438b | ||
|
|
afa4fe9820 | ||
|
|
a66df9ab0f | ||
|
|
1339f3e77e | ||
|
|
e29523e248 | ||
|
|
44eac36e05 | ||
|
|
050e9b0d4a | ||
|
|
25ac89e9b5 | ||
|
|
d86d1f9f52 | ||
|
|
fd19280df9 | ||
|
|
b7dbda497a | ||
|
|
815543b77e | ||
|
|
0948e0359f | ||
|
|
2830c7dab8 | ||
|
|
a03c13f304 | ||
|
|
ef64226ed0 | ||
|
|
ed39a130a8 | ||
|
|
21687a1f58 | ||
|
|
bf75f77eee | ||
|
|
39526e5360 | ||
|
|
8e40a55d5d | ||
|
|
20927674da | ||
|
|
8464307a43 | ||
|
|
546cdbde0d | ||
|
|
ada4e6d5e9 | ||
|
|
d6554702d8 | ||
|
|
b083c33857 | ||
|
|
f8309b007c | ||
|
|
ac672e4b8f | ||
|
|
c387c03944 | ||
|
|
fb47bfe92a | ||
|
|
5b9be7c169 | ||
|
|
6d1f6a1084 | ||
|
|
f9ed2aa17f | ||
|
|
35118da606 | ||
|
|
498477c5a2 | ||
|
|
3a84e4a0b4 | ||
|
|
4391457a96 | ||
|
|
68b4bc9d9e | ||
|
|
b052c9f562 | ||
|
|
a19597626b |
@@ -293,6 +293,12 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
|
||||
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
|
||||
|
||||
**Why Heap Allocation Matters:**
|
||||
|
||||
ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over time, repeated allocations of different sizes fragment the heap. Failures happen when the largest contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by this.
|
||||
|
||||
**Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`, `std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based APIs.
|
||||
|
||||
**STL Container Guidelines:**
|
||||
|
||||
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
|
||||
@@ -322,15 +328,15 @@ This document provides essential context for AI models interacting with this pro
|
||||
std::array<uint8_t, 256> buffer;
|
||||
```
|
||||
|
||||
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface.
|
||||
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for compile-time fixed size with `push_back()` interface (no dynamic allocation).
|
||||
```cpp
|
||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
||||
std::vector<ServiceRecord> services;
|
||||
services.reserve(5); // Still includes reallocation machinery
|
||||
|
||||
// Good - compile-time fixed size, stack allocated, no reallocation machinery
|
||||
StaticVector<ServiceRecord, MAX_SERVICES> services; // Allocates all MAX_SERVICES on stack
|
||||
services.push_back(record1); // Tracks count but all slots allocated
|
||||
// Good - compile-time fixed size, no dynamic allocation
|
||||
StaticVector<ServiceRecord, MAX_SERVICES> services;
|
||||
services.push_back(record1);
|
||||
```
|
||||
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
|
||||
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
|
||||
@@ -372,22 +378,21 @@ This document provides essential context for AI models interacting with this pro
|
||||
```
|
||||
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
|
||||
|
||||
5. **Detection:** Look for these patterns in compiler output:
|
||||
5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
|
||||
|
||||
6. **Detection:** Look for these patterns in compiler output:
|
||||
- Large code sections with STL symbols (vector, map, set)
|
||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
||||
- Red-black tree code (`rb_tree`, `_Rb_tree`)
|
||||
- Hash table infrastructure (`unordered_map`, `hash`)
|
||||
|
||||
**When to optimize:**
|
||||
**Prioritize optimization effort for:**
|
||||
- Core components (API, network, logger)
|
||||
- Widely-used components (mdns, wifi, ble)
|
||||
- Components causing flash size complaints
|
||||
|
||||
**When not to optimize:**
|
||||
- Single-use niche components
|
||||
- Code where readability matters more than bytes
|
||||
- Already using appropriate containers
|
||||
Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`).
|
||||
|
||||
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
191a0e6ab5842d153dd77a2023bc5742f9d4333c334de8d81b57f2b8d4d4b65e
|
||||
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.10
|
||||
rev: v0.14.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -249,6 +249,7 @@ esphome/components/ina260/* @mreditor97
|
||||
esphome/components/ina2xx_base/* @latonita
|
||||
esphome/components/ina2xx_i2c/* @latonita
|
||||
esphome/components/ina2xx_spi/* @latonita
|
||||
esphome/components/infrared/* @kbx81
|
||||
esphome/components/inkbird_ibsth1_mini/* @fkirill
|
||||
esphome/components/inkplate/* @jesserockz @JosipKuci
|
||||
esphome/components/integration/* @OttoWinter
|
||||
|
||||
@@ -62,6 +62,9 @@ from esphome.util import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Maximum buffer size for serial log reading to prevent unbounded memory growth
|
||||
SERIAL_BUFFER_MAX_SIZE = 65536
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
_NON_COMPONENT_KEYS = frozenset(
|
||||
{
|
||||
@@ -431,25 +434,37 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
while tries < 5:
|
||||
try:
|
||||
with ser:
|
||||
buffer = b""
|
||||
ser.timeout = 0.1 # 100ms timeout for non-blocking reads
|
||||
while True:
|
||||
try:
|
||||
raw = ser.readline()
|
||||
# Read all available data and timestamp it
|
||||
chunk = ser.read(ser.in_waiting or 1)
|
||||
if not chunk:
|
||||
continue
|
||||
time_ = datetime.now()
|
||||
milliseconds = time_.microsecond // 1000
|
||||
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]"
|
||||
|
||||
# Add to buffer and process complete lines
|
||||
# Limit buffer size to prevent unbounded memory growth
|
||||
# if device sends data without newlines
|
||||
buffer += chunk
|
||||
if len(buffer) > SERIAL_BUFFER_MAX_SIZE:
|
||||
buffer = buffer[-SERIAL_BUFFER_MAX_SIZE:]
|
||||
while b"\n" in buffer:
|
||||
raw_line, buffer = buffer.split(b"\n", 1)
|
||||
line = raw_line.replace(b"\r", b"").decode(
|
||||
"utf8", "backslashreplace"
|
||||
)
|
||||
safe_print(parser.parse_line(line, time_str))
|
||||
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
except serial.SerialException:
|
||||
_LOGGER.error("Serial port closed!")
|
||||
return 0
|
||||
line = (
|
||||
raw.replace(b"\r", b"")
|
||||
.replace(b"\n", b"")
|
||||
.decode("utf8", "backslashreplace")
|
||||
)
|
||||
time_ = datetime.now()
|
||||
nanoseconds = time_.microsecond // 1000
|
||||
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
|
||||
safe_print(parser.parse_line(line, time_str))
|
||||
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
except serial.SerialException:
|
||||
tries += 1
|
||||
time.sleep(1)
|
||||
@@ -1017,6 +1032,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
idedata.objdump_path,
|
||||
idedata.readelf_path,
|
||||
external_components,
|
||||
idedata=idedata,
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from .helpers import (
|
||||
map_section_name,
|
||||
parse_symbol_line,
|
||||
)
|
||||
from .toolchain import find_tool, run_tool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.platformio_api import IDEData
|
||||
@@ -53,6 +54,9 @@ _NAMESPACE_STD = "std::"
|
||||
# Type alias for symbol information: (symbol_name, size, component)
|
||||
SymbolInfoType = tuple[str, int, str]
|
||||
|
||||
# RAM sections - symbols in these sections consume RAM
|
||||
RAM_SECTIONS = frozenset([".data", ".bss"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemorySection:
|
||||
@@ -60,7 +64,20 @@ class MemorySection:
|
||||
|
||||
name: str
|
||||
symbols: list[SymbolInfoType] = field(default_factory=list)
|
||||
total_size: int = 0
|
||||
total_size: int = 0 # Actual section size from ELF headers
|
||||
symbol_size: int = 0 # Sum of symbol sizes (may be less than total_size)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SDKSymbol:
|
||||
"""Represents a symbol from an SDK library that's not in the ELF symbol table."""
|
||||
|
||||
name: str
|
||||
size: int
|
||||
library: str # Name of the .a file (e.g., "libpp.a")
|
||||
section: str # ".bss" or ".data"
|
||||
is_local: bool # True if static/local symbol (lowercase in nm output)
|
||||
demangled: str = "" # Demangled name (populated after analysis)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -118,6 +135,10 @@ class MemoryAnalyzer:
|
||||
self.objdump_path = objdump_path or "objdump"
|
||||
self.readelf_path = readelf_path or "readelf"
|
||||
self.external_components = external_components or set()
|
||||
self._idedata = idedata
|
||||
|
||||
# Derive nm path from objdump path using shared toolchain utility
|
||||
self.nm_path = find_tool("nm", self.objdump_path)
|
||||
|
||||
self.sections: dict[str, MemorySection] = {}
|
||||
self.components: dict[str, ComponentMemory] = defaultdict(
|
||||
@@ -128,15 +149,25 @@ class MemoryAnalyzer:
|
||||
self._esphome_core_symbols: list[
|
||||
tuple[str, str, int]
|
||||
] = [] # Track core symbols
|
||||
self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict(
|
||||
# Track symbols for all components: (symbol_name, demangled, size, section)
|
||||
self._component_symbols: dict[str, list[tuple[str, str, int, str]]] = (
|
||||
defaultdict(list)
|
||||
)
|
||||
# Track RAM symbols separately for detailed analysis: (symbol_name, demangled, size, section)
|
||||
self._ram_symbols: dict[str, list[tuple[str, str, int, str]]] = defaultdict(
|
||||
list
|
||||
) # Track symbols for all components
|
||||
)
|
||||
# Track ELF symbol names for SDK cross-reference
|
||||
self._elf_symbol_names: set[str] = set()
|
||||
# SDK symbols not in ELF (static/local symbols from closed-source libs)
|
||||
self._sdk_symbols: list[SDKSymbol] = []
|
||||
|
||||
def analyze(self) -> dict[str, ComponentMemory]:
|
||||
"""Analyze the ELF file and return component memory usage."""
|
||||
self._parse_sections()
|
||||
self._parse_symbols()
|
||||
self._categorize_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
return dict(self.components)
|
||||
|
||||
def _parse_sections(self) -> None:
|
||||
@@ -190,6 +221,8 @@ class MemoryAnalyzer:
|
||||
continue
|
||||
|
||||
self.sections[section].symbols.append((name, size, ""))
|
||||
self.sections[section].symbol_size += size
|
||||
self._elf_symbol_names.add(name)
|
||||
seen_addresses.add(address)
|
||||
|
||||
def _categorize_symbols(self) -> None:
|
||||
@@ -233,8 +266,13 @@ class MemoryAnalyzer:
|
||||
if size > 0:
|
||||
demangled = self._demangle_symbol(symbol_name)
|
||||
self._component_symbols[component].append(
|
||||
(symbol_name, demangled, size)
|
||||
(symbol_name, demangled, size, section_name)
|
||||
)
|
||||
# Track RAM symbols separately for detailed RAM analysis
|
||||
if section_name in RAM_SECTIONS:
|
||||
self._ram_symbols[component].append(
|
||||
(symbol_name, demangled, size, section_name)
|
||||
)
|
||||
|
||||
def _identify_component(self, symbol_name: str) -> str:
|
||||
"""Identify which component a symbol belongs to."""
|
||||
@@ -328,6 +366,247 @@ class MemoryAnalyzer:
|
||||
|
||||
return "Other Core"
|
||||
|
||||
def get_unattributed_ram(self) -> tuple[int, int, int]:
|
||||
"""Get unattributed RAM sizes (SDK/framework overhead).
|
||||
|
||||
Returns:
|
||||
Tuple of (unattributed_bss, unattributed_data, total_unattributed)
|
||||
These are bytes in RAM sections that have no corresponding symbols.
|
||||
"""
|
||||
bss_section = self.sections.get(".bss")
|
||||
data_section = self.sections.get(".data")
|
||||
|
||||
unattributed_bss = 0
|
||||
unattributed_data = 0
|
||||
|
||||
if bss_section:
|
||||
unattributed_bss = max(0, bss_section.total_size - bss_section.symbol_size)
|
||||
if data_section:
|
||||
unattributed_data = max(
|
||||
0, data_section.total_size - data_section.symbol_size
|
||||
)
|
||||
|
||||
return unattributed_bss, unattributed_data, unattributed_bss + unattributed_data
|
||||
|
||||
def _find_sdk_library_dirs(self) -> list[Path]:
|
||||
"""Find SDK library directories based on platform.
|
||||
|
||||
Returns:
|
||||
List of paths to SDK library directories containing .a files.
|
||||
"""
|
||||
sdk_dirs: list[Path] = []
|
||||
|
||||
if self._idedata is None:
|
||||
return sdk_dirs
|
||||
|
||||
# Get the CC path to determine the framework location
|
||||
cc_path = getattr(self._idedata, "cc_path", None)
|
||||
if not cc_path:
|
||||
return sdk_dirs
|
||||
|
||||
cc_path = Path(cc_path)
|
||||
|
||||
# For ESP8266 Arduino framework
|
||||
# CC is like: ~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gcc
|
||||
# SDK libs are in: ~/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lib/
|
||||
if "xtensa-lx106" in str(cc_path):
|
||||
platformio_dir = cc_path.parent.parent.parent
|
||||
esp8266_sdk = (
|
||||
platformio_dir
|
||||
/ "framework-arduinoespressif8266"
|
||||
/ "tools"
|
||||
/ "sdk"
|
||||
/ "lib"
|
||||
)
|
||||
if esp8266_sdk.exists():
|
||||
sdk_dirs.append(esp8266_sdk)
|
||||
# Also check for NONOSDK subdirectories (closed-source libs)
|
||||
sdk_dirs.extend(
|
||||
subdir
|
||||
for subdir in esp8266_sdk.iterdir()
|
||||
if subdir.is_dir() and subdir.name.startswith("NONOSDK")
|
||||
)
|
||||
|
||||
# For ESP32 IDF framework
|
||||
# CC is like: ~/.platformio/packages/toolchain-xtensa-esp-elf/bin/xtensa-esp32-elf-gcc
|
||||
# or: ~/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc
|
||||
elif "xtensa-esp" in str(cc_path) or "riscv32-esp" in str(cc_path):
|
||||
# Detect ESP32 variant from CC path or defines
|
||||
variant = self._detect_esp32_variant()
|
||||
if variant:
|
||||
platformio_dir = cc_path.parent.parent.parent
|
||||
espidf_dir = platformio_dir / "framework-espidf" / "components"
|
||||
if espidf_dir.exists():
|
||||
# Find all directories named after the variant that contain .a files
|
||||
# This handles various ESP-IDF library layouts:
|
||||
# - components/*/lib/<variant>/
|
||||
# - components/*/<variant>/
|
||||
# - components/*/lib/lib/<variant>/
|
||||
# - components/*/*/lib_*/<variant>/
|
||||
sdk_dirs.extend(
|
||||
variant_dir
|
||||
for variant_dir in espidf_dir.rglob(variant)
|
||||
if variant_dir.is_dir() and any(variant_dir.glob("*.a"))
|
||||
)
|
||||
|
||||
return sdk_dirs
|
||||
|
||||
def _detect_esp32_variant(self) -> str | None:
|
||||
"""Detect ESP32 variant from idedata defines.
|
||||
|
||||
Returns:
|
||||
Variant string like 'esp32', 'esp32s2', 'esp32c3', etc. or None.
|
||||
"""
|
||||
if self._idedata is None:
|
||||
return None
|
||||
|
||||
defines = getattr(self._idedata, "defines", [])
|
||||
if not defines:
|
||||
return None
|
||||
|
||||
# ESPHome always adds USE_ESP32_VARIANT_xxx defines
|
||||
variant_prefix = "USE_ESP32_VARIANT_"
|
||||
for define in defines:
|
||||
if define.startswith(variant_prefix):
|
||||
# Extract variant name and convert to lowercase
|
||||
# USE_ESP32_VARIANT_ESP32 -> esp32
|
||||
# USE_ESP32_VARIANT_ESP32S3 -> esp32s3
|
||||
return define[len(variant_prefix) :].lower()
|
||||
|
||||
return None
|
||||
|
||||
def _parse_sdk_library(
|
||||
self, lib_path: Path
|
||||
) -> tuple[list[tuple[str, int, str, bool]], set[str]]:
|
||||
"""Parse a single SDK library for symbols.
|
||||
|
||||
Args:
|
||||
lib_path: Path to the .a library file
|
||||
|
||||
Returns:
|
||||
Tuple of:
|
||||
- List of BSS/DATA symbols: (symbol_name, size, section, is_local)
|
||||
- Set of global BSS/DATA symbol names (for checking if RAM is linked)
|
||||
"""
|
||||
ram_symbols: list[tuple[str, int, str, bool]] = []
|
||||
global_ram_symbols: set[str] = set()
|
||||
|
||||
result = run_tool([self.nm_path, "--size-sort", str(lib_path)], timeout=10)
|
||||
if result is None:
|
||||
return ram_symbols, global_ram_symbols
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
size = int(parts[0], 16)
|
||||
sym_type = parts[1]
|
||||
name = parts[2]
|
||||
|
||||
# Only collect BSS (b/B) and DATA (d/D) for RAM analysis
|
||||
if sym_type in ("b", "B"):
|
||||
section = ".bss"
|
||||
is_local = sym_type == "b"
|
||||
ram_symbols.append((name, size, section, is_local))
|
||||
# Track global RAM symbols (B/D) for linking check
|
||||
if sym_type == "B":
|
||||
global_ram_symbols.add(name)
|
||||
elif sym_type in ("d", "D"):
|
||||
section = ".data"
|
||||
is_local = sym_type == "d"
|
||||
ram_symbols.append((name, size, section, is_local))
|
||||
if sym_type == "D":
|
||||
global_ram_symbols.add(name)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
return ram_symbols, global_ram_symbols
|
||||
|
||||
def _analyze_sdk_libraries(self) -> None:
|
||||
"""Analyze SDK libraries to find symbols not in the ELF.
|
||||
|
||||
This finds static/local symbols from closed-source SDK libraries
|
||||
that consume RAM but don't appear in the final ELF symbol table.
|
||||
Only includes symbols from libraries that have RAM actually linked
|
||||
(at least one global BSS/DATA symbol in the ELF).
|
||||
"""
|
||||
sdk_dirs = self._find_sdk_library_dirs()
|
||||
if not sdk_dirs:
|
||||
_LOGGER.debug("No SDK library directories found")
|
||||
return
|
||||
|
||||
_LOGGER.debug("Analyzing SDK libraries in %d directories", len(sdk_dirs))
|
||||
|
||||
# Track seen symbols to avoid duplicates from multiple SDK versions
|
||||
seen_symbols: set[str] = set()
|
||||
|
||||
for sdk_dir in sdk_dirs:
|
||||
for lib_path in sorted(sdk_dir.glob("*.a")):
|
||||
lib_name = lib_path.name
|
||||
ram_symbols, global_ram_symbols = self._parse_sdk_library(lib_path)
|
||||
|
||||
# Check if this library's RAM is actually linked by seeing if any
|
||||
# of its global BSS/DATA symbols appear in the ELF
|
||||
if not global_ram_symbols & self._elf_symbol_names:
|
||||
# No RAM from this library is in the ELF - skip it
|
||||
continue
|
||||
|
||||
for name, size, section, is_local in ram_symbols:
|
||||
# Skip if already in ELF or already seen from another lib
|
||||
if name in self._elf_symbol_names or name in seen_symbols:
|
||||
continue
|
||||
|
||||
# Only track symbols with non-zero size
|
||||
if size > 0:
|
||||
self._sdk_symbols.append(
|
||||
SDKSymbol(
|
||||
name=name,
|
||||
size=size,
|
||||
library=lib_name,
|
||||
section=section,
|
||||
is_local=is_local,
|
||||
)
|
||||
)
|
||||
seen_symbols.add(name)
|
||||
|
||||
# Demangle SDK symbols for better readability
|
||||
if self._sdk_symbols:
|
||||
sdk_names = [sym.name for sym in self._sdk_symbols]
|
||||
demangled_map = batch_demangle(sdk_names, objdump_path=self.objdump_path)
|
||||
for sym in self._sdk_symbols:
|
||||
sym.demangled = demangled_map.get(sym.name, sym.name)
|
||||
|
||||
# Sort by size descending for reporting
|
||||
self._sdk_symbols.sort(key=lambda s: s.size, reverse=True)
|
||||
|
||||
total_sdk_ram = sum(s.size for s in self._sdk_symbols)
|
||||
_LOGGER.debug(
|
||||
"Found %d SDK symbols not in ELF, totaling %d bytes",
|
||||
len(self._sdk_symbols),
|
||||
total_sdk_ram,
|
||||
)
|
||||
|
||||
def get_sdk_ram_symbols(self) -> list[SDKSymbol]:
|
||||
"""Get SDK symbols that consume RAM but aren't in the ELF symbol table.
|
||||
|
||||
Returns:
|
||||
List of SDKSymbol objects sorted by size descending.
|
||||
"""
|
||||
return self._sdk_symbols
|
||||
|
||||
def get_sdk_ram_by_library(self) -> dict[str, list[SDKSymbol]]:
|
||||
"""Get SDK RAM symbols grouped by library.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping library name to list of symbols.
|
||||
"""
|
||||
by_lib: dict[str, list[SDKSymbol]] = defaultdict(list)
|
||||
for sym in self._sdk_symbols:
|
||||
by_lib[sym.library].append(sym)
|
||||
return dict(by_lib)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from .cli import main
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
"""CLI interface for memory analysis with report generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import (
|
||||
_COMPONENT_API,
|
||||
_COMPONENT_CORE,
|
||||
_COMPONENT_PREFIX_ESPHOME,
|
||||
_COMPONENT_PREFIX_EXTERNAL,
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ComponentMemory
|
||||
|
||||
|
||||
class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
"""Memory analyzer with CLI-specific report generation."""
|
||||
@@ -19,6 +27,8 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
SYMBOL_SIZE_THRESHOLD: int = (
|
||||
100 # Show symbols larger than this in detailed analysis
|
||||
)
|
||||
# Lower threshold for RAM symbols (RAM is more constrained)
|
||||
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
|
||||
|
||||
# Column width constants
|
||||
COL_COMPONENT: int = 29
|
||||
@@ -83,6 +93,60 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
COL_CORE_PERCENT,
|
||||
)
|
||||
|
||||
def _add_section_header(self, lines: list[str], title: str) -> None:
|
||||
"""Add a section header with title centered between separator lines."""
|
||||
lines.append("")
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append(title.center(self.TABLE_WIDTH))
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append("")
|
||||
|
||||
def _add_top_consumers(
|
||||
self,
|
||||
lines: list[str],
|
||||
title: str,
|
||||
components: list[tuple[str, ComponentMemory]],
|
||||
get_size: Callable[[ComponentMemory], int],
|
||||
total: int,
|
||||
memory_type: str,
|
||||
limit: int = 25,
|
||||
) -> None:
|
||||
"""Add a formatted list of top memory consumers to the report.
|
||||
|
||||
Args:
|
||||
lines: List of report lines to append the output to.
|
||||
title: Section title to print before the list.
|
||||
components: Sequence of (name, ComponentMemory) tuples to analyze.
|
||||
get_size: Callable that takes a ComponentMemory and returns the
|
||||
size in bytes to use for ranking and display.
|
||||
total: Total size in bytes for computing percentage usage.
|
||||
memory_type: Label for the memory region (e.g., "flash" or "RAM").
|
||||
limit: Maximum number of components to include in the list.
|
||||
"""
|
||||
lines.append("")
|
||||
lines.append(f"{title}:")
|
||||
for i, (name, mem) in enumerate(components[:limit]):
|
||||
size = get_size(mem)
|
||||
if size > 0:
|
||||
percentage = (size / total * 100) if total > 0 else 0
|
||||
lines.append(
|
||||
f"{i + 1}. {name} ({size:,} B) - {percentage:.1f}% of analyzed {memory_type}"
|
||||
)
|
||||
|
||||
def _format_symbol_with_section(
|
||||
self, demangled: str, size: int, section: str | None = None
|
||||
) -> str:
|
||||
"""Format a symbol entry, optionally adding a RAM section label.
|
||||
|
||||
If section is one of the RAM sections (.data or .bss), a label like
|
||||
" [data]" or " [bss]" is appended. For non-RAM sections or when
|
||||
section is None, no section label is added.
|
||||
"""
|
||||
section_label = ""
|
||||
if section in RAM_SECTIONS:
|
||||
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
|
||||
return f"{demangled} ({size:,} B){section_label}"
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -123,43 +187,70 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
|
||||
)
|
||||
|
||||
# Top consumers
|
||||
lines.append("")
|
||||
lines.append("Top Flash Consumers:")
|
||||
for i, (name, mem) in enumerate(components[:25]):
|
||||
if mem.flash_total > 0:
|
||||
percentage = (
|
||||
(mem.flash_total / total_flash * 100) if total_flash > 0 else 0
|
||||
)
|
||||
lines.append(
|
||||
f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append("Top RAM Consumers:")
|
||||
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
|
||||
for i, (name, mem) in enumerate(ram_components[:25]):
|
||||
if mem.ram_total > 0:
|
||||
percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0
|
||||
lines.append(
|
||||
f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
# Show unattributed RAM (SDK/framework overhead)
|
||||
unattributed_bss, unattributed_data, unattributed_total = (
|
||||
self.get_unattributed_ram()
|
||||
)
|
||||
if unattributed_total > 0:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"Unattributed RAM: {unattributed_total:,} B (SDK/framework overhead)"
|
||||
)
|
||||
if unattributed_bss > 0 and unattributed_data > 0:
|
||||
lines.append(
|
||||
f" .bss: {unattributed_bss:,} B | .data: {unattributed_data:,} B"
|
||||
)
|
||||
|
||||
# Show SDK symbol breakdown if available
|
||||
sdk_by_lib = self.get_sdk_ram_by_library()
|
||||
if sdk_by_lib:
|
||||
lines.append("")
|
||||
lines.append("SDK library breakdown (static symbols not in ELF):")
|
||||
# Sort libraries by total size
|
||||
lib_totals = [
|
||||
(lib, sum(s.size for s in syms), syms)
|
||||
for lib, syms in sdk_by_lib.items()
|
||||
]
|
||||
lib_totals.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for lib_name, lib_total, syms in lib_totals:
|
||||
if lib_total == 0:
|
||||
continue
|
||||
lines.append(f" {lib_name}: {lib_total:,} B")
|
||||
# Show top symbols from this library
|
||||
for sym in sorted(syms, key=lambda s: s.size, reverse=True)[:3]:
|
||||
section_label = sym.section.lstrip(".")
|
||||
# Use demangled name (falls back to original if not demangled)
|
||||
display_name = sym.demangled or sym.name
|
||||
if len(display_name) > 50:
|
||||
display_name = f"{display_name[:47]}..."
|
||||
lines.append(
|
||||
f" {sym.size:>6,} B [{section_label}] {display_name}"
|
||||
)
|
||||
|
||||
# Top consumers
|
||||
self._add_top_consumers(
|
||||
lines,
|
||||
"Top Flash Consumers",
|
||||
components,
|
||||
lambda m: m.flash_total,
|
||||
total_flash,
|
||||
"flash",
|
||||
)
|
||||
|
||||
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
|
||||
self._add_top_consumers(
|
||||
lines,
|
||||
"Top RAM Consumers",
|
||||
ram_components,
|
||||
lambda m: m.ram_total,
|
||||
total_ram,
|
||||
"RAM",
|
||||
)
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
|
||||
# Add ESPHome core detailed analysis if there are core symbols
|
||||
if self._esphome_core_symbols:
|
||||
lines.append("")
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append(
|
||||
f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH)
|
||||
)
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append("")
|
||||
self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis")
|
||||
|
||||
# Group core symbols by subcategory
|
||||
core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict(
|
||||
@@ -211,7 +302,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
|
||||
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
|
||||
# Core symbols only track (symbol, demangled, size) without section info,
|
||||
# so we don't show section labels here
|
||||
lines.append(
|
||||
f"{i + 1}. {self._format_symbol_with_section(demangled, size)}"
|
||||
)
|
||||
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
|
||||
@@ -267,11 +362,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
for comp_name, comp_mem in components_to_analyze:
|
||||
if not (comp_symbols := self._component_symbols.get(comp_name, [])):
|
||||
continue
|
||||
lines.append("")
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append("")
|
||||
self._add_section_header(lines, f"{comp_name} Detailed Analysis")
|
||||
|
||||
# Sort symbols by size
|
||||
sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
|
||||
@@ -282,19 +373,69 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
|
||||
# Show all symbols above threshold for better visibility
|
||||
large_symbols = [
|
||||
(sym, dem, size)
|
||||
for sym, dem, size in sorted_symbols
|
||||
(sym, dem, size, sec)
|
||||
for sym, dem, size, sec in sorted_symbols
|
||||
if size > self.SYMBOL_SIZE_THRESHOLD
|
||||
]
|
||||
|
||||
lines.append(
|
||||
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size) in enumerate(large_symbols):
|
||||
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
|
||||
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
|
||||
lines.append(
|
||||
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
|
||||
)
|
||||
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
|
||||
# Detailed RAM analysis by component (at end, before RAM strings analysis)
|
||||
self._add_section_header(lines, "RAM Symbol Analysis by Component")
|
||||
|
||||
# Show top 15 RAM consumers with their large symbols
|
||||
for name, mem in ram_components[:15]:
|
||||
if mem.ram_total == 0:
|
||||
continue
|
||||
ram_syms = self._ram_symbols.get(name, [])
|
||||
if not ram_syms:
|
||||
continue
|
||||
|
||||
# Sort by size descending
|
||||
sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True)
|
||||
large_ram_syms = [
|
||||
s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
|
||||
]
|
||||
|
||||
lines.append(f"{name} ({mem.ram_total:,} B total RAM):")
|
||||
|
||||
# Show breakdown by section type
|
||||
data_size = sum(s[2] for s in ram_syms if s[3] == ".data")
|
||||
bss_size = sum(s[2] for s in ram_syms if s[3] == ".bss")
|
||||
lines.append(f" .data (initialized): {data_size:,} B")
|
||||
lines.append(f" .bss (uninitialized): {bss_size:,} B")
|
||||
|
||||
if large_ram_syms:
|
||||
lines.append(
|
||||
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
|
||||
)
|
||||
for symbol, demangled, size, section in large_ram_syms[:10]:
|
||||
# Format section label consistently by stripping leading dot
|
||||
section_label = section.lstrip(".") if section else ""
|
||||
# Add ellipsis if name is truncated
|
||||
demangled_display = (
|
||||
f"{demangled[:70]}..." if len(demangled) > 70 else demangled
|
||||
)
|
||||
lines.append(
|
||||
f" {size:>6,} B [{section_label}] {demangled_display}"
|
||||
)
|
||||
if len(large_ram_syms) > 10:
|
||||
lines.append(f" ... and {len(large_ram_syms) - 10} more")
|
||||
lines.append("")
|
||||
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
)
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
||||
|
||||
@@ -7,11 +7,13 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
|
||||
|
||||
# Section mapping for ELF file sections
|
||||
# Maps standard section names to their various platform-specific variants
|
||||
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
|
||||
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
|
||||
SECTION_MAPPING = {
|
||||
".text": frozenset([".text", ".iram"]),
|
||||
".rodata": frozenset([".rodata"]),
|
||||
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
|
||||
".data": frozenset([".data", ".dram"]),
|
||||
".bss": frozenset([".bss"]),
|
||||
}
|
||||
|
||||
# Section to ComponentMemory attribute mapping
|
||||
|
||||
@@ -5,6 +5,10 @@ from __future__ import annotations
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,3 +59,35 @@ def find_tool(
|
||||
|
||||
_LOGGER.warning("Could not find %s tool", tool_name)
|
||||
return None
|
||||
|
||||
|
||||
def run_tool(
|
||||
cmd: Sequence[str],
|
||||
timeout: int = 30,
|
||||
) -> subprocess.CompletedProcess[str] | None:
|
||||
"""Run a toolchain command and return the result.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments to run
|
||||
timeout: Timeout in seconds
|
||||
|
||||
Returns:
|
||||
CompletedProcess on success, None on failure
|
||||
"""
|
||||
try:
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
_LOGGER.warning("Command timed out: %s", " ".join(cmd))
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning("Command not found: %s", cmd[0])
|
||||
return None
|
||||
except OSError as e:
|
||||
_LOGGER.warning("Failed to run command %s: %s", cmd[0], e)
|
||||
return None
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "ac_dimmer.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -9,12 +7,12 @@
|
||||
#ifdef USE_ESP8266
|
||||
#include <core_esp8266_waveform.h>
|
||||
#endif
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#include <esp32-hal-timer.h>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "hw_timer_esp_idf.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace ac_dimmer {
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
static const char *const TAG = "ac_dimmer";
|
||||
|
||||
@@ -27,7 +25,14 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no
|
||||
/// However other factors like gate driver propagation time
|
||||
/// are also considered and a really low value is not important
|
||||
/// See also: https://github.com/esphome/issues/issues/1632
|
||||
static const uint32_t GATE_ENABLE_TIME = 50;
|
||||
static constexpr uint32_t GATE_ENABLE_TIME = 50;
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/// Timer frequency in Hz (1 MHz = 1µs resolution)
|
||||
static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000;
|
||||
/// Timer interrupt interval in microseconds
|
||||
static constexpr uint64_t TIMER_INTERVAL_US = 50;
|
||||
#endif
|
||||
|
||||
/// Function called from timer interrupt
|
||||
/// Input is current time in microseconds (micros())
|
||||
@@ -154,7 +159,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
|
||||
#ifdef USE_ESP32
|
||||
// ESP32 implementation, uses basically the same code but needs to wrap
|
||||
// timer_interrupt() function to auto-reschedule
|
||||
static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
|
||||
#endif
|
||||
|
||||
@@ -194,15 +199,15 @@ void AcDimmer::setup() {
|
||||
setTimer1Callback(&timer_interrupt);
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
// timer frequency of 1mhz
|
||||
dimmer_timer = timerBegin(1000000);
|
||||
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
|
||||
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
|
||||
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
|
||||
// For ESP32, we can't use dynamic interval calculation because the timerX functions
|
||||
// are not callable from ISR (placed in flash storage).
|
||||
// Here we just use an interrupt firing every 50 µs.
|
||||
timerAlarm(dimmer_timer, 50, true, 0);
|
||||
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AcDimmer::write_state(float state) {
|
||||
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
|
||||
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
|
||||
@@ -210,6 +215,7 @@ void AcDimmer::write_state(float state) {
|
||||
this->store_.init_cycle = this->init_with_half_cycle_;
|
||||
this->store_.value = new_value;
|
||||
}
|
||||
|
||||
void AcDimmer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"AcDimmer:\n"
|
||||
@@ -230,7 +236,4 @@ void AcDimmer::dump_config() {
|
||||
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
|
||||
}
|
||||
|
||||
} // namespace ac_dimmer
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
} // namespace esphome::ac_dimmer
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ac_dimmer {
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
|
||||
|
||||
@@ -64,7 +61,4 @@ class AcDimmer : public output::FloatOutput, public Component {
|
||||
DimMethod method_;
|
||||
};
|
||||
|
||||
} // namespace ac_dimmer
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
} // namespace esphome::ac_dimmer
|
||||
|
||||
152
esphome/components/ac_dimmer/hw_timer_esp_idf.cpp
Normal file
152
esphome/components/ac_dimmer/hw_timer_esp_idf.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "hw_timer_esp_idf.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "driver/gptimer.h"
|
||||
#include "esp_clk_tree.h"
|
||||
#include "soc/clk_tree_defs.h"
|
||||
|
||||
static const char *const TAG = "hw_timer_esp_idf";
|
||||
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
// GPTimer divider constraints from ESP-IDF documentation
|
||||
static constexpr uint32_t GPTIMER_DIVIDER_MIN = 2;
|
||||
static constexpr uint32_t GPTIMER_DIVIDER_MAX = 65536;
|
||||
|
||||
using voidFuncPtr = void (*)();
|
||||
using voidFuncPtrArg = void (*)(void *);
|
||||
|
||||
struct InterruptConfigT {
|
||||
voidFuncPtr fn{nullptr};
|
||||
void *arg{nullptr};
|
||||
};
|
||||
|
||||
struct HWTimer {
|
||||
gptimer_handle_t timer_handle{nullptr};
|
||||
InterruptConfigT interrupt_handle{};
|
||||
bool timer_started{false};
|
||||
};
|
||||
|
||||
HWTimer *timer_begin(uint32_t frequency) {
|
||||
esp_err_t err = ESP_OK;
|
||||
uint32_t counter_src_hz = 0;
|
||||
uint32_t divider = 0;
|
||||
soc_module_clk_t clk;
|
||||
for (auto clk_candidate : SOC_GPTIMER_CLKS) {
|
||||
clk = clk_candidate;
|
||||
esp_clk_tree_src_get_freq_hz(clk, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &counter_src_hz);
|
||||
divider = counter_src_hz / frequency;
|
||||
if ((divider >= GPTIMER_DIVIDER_MIN) && (divider <= GPTIMER_DIVIDER_MAX)) {
|
||||
break;
|
||||
} else {
|
||||
divider = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (divider == 0) {
|
||||
ESP_LOGE(TAG, "Resolution not possible; aborting");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
gptimer_config_t config = {
|
||||
.clk_src = static_cast<gptimer_clock_source_t>(clk),
|
||||
.direction = GPTIMER_COUNT_UP,
|
||||
.resolution_hz = frequency,
|
||||
.flags = {.intr_shared = true},
|
||||
};
|
||||
|
||||
HWTimer *timer = new HWTimer();
|
||||
|
||||
err = gptimer_new_timer(&config, &timer->timer_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "GPTimer creation failed; error %d", err);
|
||||
delete timer;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
err = gptimer_enable(timer->timer_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "GPTimer enable failed; error %d", err);
|
||||
gptimer_del_timer(timer->timer_handle);
|
||||
delete timer;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
err = gptimer_start(timer->timer_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "GPTimer start failed; error %d", err);
|
||||
gptimer_disable(timer->timer_handle);
|
||||
gptimer_del_timer(timer->timer_handle);
|
||||
delete timer;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
timer->timer_started = true;
|
||||
return timer;
|
||||
}
|
||||
|
||||
bool IRAM_ATTR timer_fn_wrapper(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *args) {
|
||||
auto *isr = static_cast<InterruptConfigT *>(args);
|
||||
if (isr->fn) {
|
||||
if (isr->arg) {
|
||||
reinterpret_cast<voidFuncPtrArg>(isr->fn)(isr->arg);
|
||||
} else {
|
||||
isr->fn();
|
||||
}
|
||||
}
|
||||
// Return false to indicate that no higher-priority task was woken and no context switch is requested.
|
||||
return false;
|
||||
}
|
||||
|
||||
static void timer_attach_interrupt_functional_arg(HWTimer *timer, void (*user_func)(void *), void *arg) {
|
||||
if (timer == nullptr) {
|
||||
ESP_LOGE(TAG, "Timer handle is nullptr");
|
||||
return;
|
||||
}
|
||||
gptimer_event_callbacks_t cbs = {
|
||||
.on_alarm = timer_fn_wrapper,
|
||||
};
|
||||
|
||||
timer->interrupt_handle.fn = reinterpret_cast<voidFuncPtr>(user_func);
|
||||
timer->interrupt_handle.arg = arg;
|
||||
|
||||
if (timer->timer_started) {
|
||||
gptimer_stop(timer->timer_handle);
|
||||
}
|
||||
gptimer_disable(timer->timer_handle);
|
||||
esp_err_t err = gptimer_register_event_callbacks(timer->timer_handle, &cbs, &timer->interrupt_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Timer Attach Interrupt failed; error %d", err);
|
||||
}
|
||||
gptimer_enable(timer->timer_handle);
|
||||
if (timer->timer_started) {
|
||||
gptimer_start(timer->timer_handle);
|
||||
}
|
||||
}
|
||||
|
||||
void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func) {
|
||||
timer_attach_interrupt_functional_arg(timer, reinterpret_cast<voidFuncPtrArg>(user_func), nullptr);
|
||||
}
|
||||
|
||||
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count) {
|
||||
if (timer == nullptr) {
|
||||
ESP_LOGE(TAG, "Timer handle is nullptr");
|
||||
return;
|
||||
}
|
||||
gptimer_alarm_config_t alarm_cfg = {
|
||||
.alarm_count = alarm_value,
|
||||
.reload_count = reload_count,
|
||||
.flags = {.auto_reload_on_alarm = autoreload},
|
||||
};
|
||||
esp_err_t err = gptimer_set_alarm_action(timer->timer_handle, &alarm_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Timer Alarm Write failed; error %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ac_dimmer
|
||||
#endif
|
||||
17
esphome/components/ac_dimmer/hw_timer_esp_idf.h
Normal file
17
esphome/components/ac_dimmer/hw_timer_esp_idf.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "driver/gptimer_types.h"
|
||||
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
struct HWTimer;
|
||||
|
||||
HWTimer *timer_begin(uint32_t frequency);
|
||||
|
||||
void timer_attach_interrupt(HWTimer *timer, void (*user_func)());
|
||||
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count);
|
||||
|
||||
} // namespace esphome::ac_dimmer
|
||||
|
||||
#endif
|
||||
@@ -32,7 +32,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_with_arduino,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ service APIConnection {
|
||||
|
||||
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
|
||||
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
|
||||
|
||||
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -763,7 +765,7 @@ message SubscribeHomeassistantServicesRequest {
|
||||
|
||||
message HomeassistantServiceMap {
|
||||
string key = 1;
|
||||
string value = 2 [(no_zero_copy) = true];
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message HomeassistantActionRequest {
|
||||
@@ -779,7 +781,7 @@ message HomeassistantActionRequest {
|
||||
bool is_event = 5;
|
||||
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
||||
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
string response_template = 8 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
}
|
||||
|
||||
// Message sent by Home Assistant to ESPHome with service call response data
|
||||
@@ -2437,3 +2439,49 @@ message ZWaveProxyRequest {
|
||||
ZWaveProxyRequestType type = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
// ==================== INFRARED ====================
|
||||
// Note: Feature and capability flag enums are defined in
|
||||
// esphome/components/infrared/infrared.h
|
||||
|
||||
// Listing of infrared instances
|
||||
message ListEntitiesInfraredResponse {
|
||||
option (id) = 135;
|
||||
option (base_class) = "InfoResponseProtoMessage";
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_INFRARED";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
bool disabled_by_default = 5;
|
||||
EntityCategory entity_category = 6;
|
||||
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
|
||||
uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags
|
||||
}
|
||||
|
||||
// Command to transmit infrared/RF data using raw timings
|
||||
message InfraredRFTransmitRawTimingsRequest {
|
||||
option (id) = 136;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2; // Key identifying the transmitter instance
|
||||
uint32 carrier_frequency = 3; // Carrier frequency in Hz
|
||||
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
|
||||
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
|
||||
}
|
||||
|
||||
// Event message for received infrared/RF data
|
||||
message InfraredRFReceiveEvent {
|
||||
option (id) = 137;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
option (no_delay) = true;
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2; // Key identifying the receiver instance
|
||||
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -376,7 +379,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
|
||||
bool is_single) {
|
||||
auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
|
||||
ListEntitiesBinarySensorResponse msg;
|
||||
msg.set_device_class(binary_sensor->get_device_class_ref());
|
||||
msg.device_class = binary_sensor->get_device_class_ref();
|
||||
msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
|
||||
return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size, is_single);
|
||||
@@ -408,7 +411,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
|
||||
msg.supports_position = traits.get_supports_position();
|
||||
msg.supports_tilt = traits.get_supports_tilt();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
msg.set_device_class(cover->get_device_class_ref());
|
||||
msg.device_class = cover->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -443,7 +446,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
|
||||
if (traits.supports_direction())
|
||||
msg.direction = static_cast<enums::FanDirection>(fan->direction);
|
||||
if (traits.supports_preset_modes() && fan->has_preset_mode())
|
||||
msg.set_preset_mode(StringRef(fan->get_preset_mode()));
|
||||
msg.preset_mode = fan->get_preset_mode();
|
||||
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
@@ -499,7 +502,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
|
||||
resp.cold_white = values.get_cold_white();
|
||||
resp.warm_white = values.get_warm_white();
|
||||
if (light->supports_effects()) {
|
||||
resp.set_effect(light->get_effect_name_ref());
|
||||
resp.effect = light->get_effect_name();
|
||||
}
|
||||
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -522,7 +525,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
effects_list.init(light_effects.size() + 1);
|
||||
effects_list.push_back("None");
|
||||
for (auto *effect : light_effects) {
|
||||
effects_list.push_back(effect->get_name());
|
||||
// c_str() is safe as effect names are null-terminated strings from codegen
|
||||
effects_list.push_back(effect->get_name().c_str());
|
||||
}
|
||||
}
|
||||
msg.effects = &effects_list;
|
||||
@@ -581,10 +585,10 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *sensor = static_cast<sensor::Sensor *>(entity);
|
||||
ListEntitiesSensorResponse msg;
|
||||
msg.set_unit_of_measurement(sensor->get_unit_of_measurement_ref());
|
||||
msg.unit_of_measurement = sensor->get_unit_of_measurement_ref();
|
||||
msg.accuracy_decimals = sensor->get_accuracy_decimals();
|
||||
msg.force_update = sensor->get_force_update();
|
||||
msg.set_device_class(sensor->get_device_class_ref());
|
||||
msg.device_class = sensor->get_device_class_ref();
|
||||
msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
|
||||
return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
@@ -611,7 +615,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
auto *a_switch = static_cast<switch_::Switch *>(entity);
|
||||
ListEntitiesSwitchResponse msg;
|
||||
msg.assumed_state = a_switch->assumed_state();
|
||||
msg.set_device_class(a_switch->get_device_class_ref());
|
||||
msg.device_class = a_switch->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -636,7 +640,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
|
||||
bool is_single) {
|
||||
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
|
||||
TextSensorStateResponse resp;
|
||||
resp.set_state(StringRef(text_sensor->state));
|
||||
resp.state = StringRef(text_sensor->state);
|
||||
resp.missing_state = !text_sensor->has_state();
|
||||
return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
@@ -645,7 +649,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
|
||||
bool is_single) {
|
||||
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
|
||||
ListEntitiesTextSensorResponse msg;
|
||||
msg.set_device_class(text_sensor->get_device_class_ref());
|
||||
msg.device_class = text_sensor->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size, is_single);
|
||||
}
|
||||
@@ -675,13 +679,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
||||
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
||||
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
|
||||
resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode()));
|
||||
resp.custom_fan_mode = climate->get_custom_fan_mode();
|
||||
}
|
||||
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
||||
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
|
||||
resp.set_custom_preset(StringRef(climate->get_custom_preset()));
|
||||
resp.custom_preset = climate->get_custom_preset();
|
||||
}
|
||||
if (traits.get_supports_swing_modes())
|
||||
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
||||
@@ -766,9 +770,9 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *number = static_cast<number::Number *>(entity);
|
||||
ListEntitiesNumberResponse msg;
|
||||
msg.set_unit_of_measurement(number->traits.get_unit_of_measurement_ref());
|
||||
msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref();
|
||||
msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
|
||||
msg.set_device_class(number->traits.get_device_class_ref());
|
||||
msg.device_class = number->traits.get_device_class_ref();
|
||||
msg.min_value = number->traits.get_min_value();
|
||||
msg.max_value = number->traits.get_max_value();
|
||||
msg.step = number->traits.get_step();
|
||||
@@ -881,7 +885,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c
|
||||
bool is_single) {
|
||||
auto *text = static_cast<text::Text *>(entity);
|
||||
TextStateResponse resp;
|
||||
resp.set_state(StringRef(text->state));
|
||||
resp.state = StringRef(text->state);
|
||||
resp.missing_state = !text->has_state();
|
||||
return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -893,7 +897,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
|
||||
msg.mode = static_cast<enums::TextMode>(text->traits.get_mode());
|
||||
msg.min_length = text->traits.get_min_length();
|
||||
msg.max_length = text->traits.get_max_length();
|
||||
msg.set_pattern(text->traits.get_pattern_ref());
|
||||
msg.pattern = text->traits.get_pattern_ref();
|
||||
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -914,7 +918,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
|
||||
bool is_single) {
|
||||
auto *select = static_cast<select::Select *>(entity);
|
||||
SelectStateResponse resp;
|
||||
resp.set_state(StringRef(select->current_option()));
|
||||
resp.state = select->current_option();
|
||||
resp.missing_state = !select->has_state();
|
||||
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -939,7 +943,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *button = static_cast<button::Button *>(entity);
|
||||
ListEntitiesButtonResponse msg;
|
||||
msg.set_device_class(button->get_device_class_ref());
|
||||
msg.device_class = button->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -1008,7 +1012,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
|
||||
auto *valve = static_cast<valve::Valve *>(entity);
|
||||
ListEntitiesValveResponse msg;
|
||||
auto traits = valve->get_traits();
|
||||
msg.set_device_class(valve->get_device_class_ref());
|
||||
msg.device_class = valve->get_device_class_ref();
|
||||
msg.assumed_state = traits.get_is_assumed_state();
|
||||
msg.supports_position = traits.get_supports_position();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
@@ -1053,7 +1057,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
for (auto &supported_format : traits.get_supported_formats()) {
|
||||
msg.supported_formats.emplace_back();
|
||||
auto &media_format = msg.supported_formats.back();
|
||||
media_format.set_format(StringRef(supported_format.format));
|
||||
media_format.format = StringRef(supported_format.format);
|
||||
media_format.sample_rate = supported_format.sample_rate;
|
||||
media_format.num_channels = supported_format.num_channels;
|
||||
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
||||
@@ -1263,8 +1267,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
|
||||
for (auto &wake_word : config.available_wake_words) {
|
||||
resp.available_wake_words.emplace_back();
|
||||
auto &resp_wake_word = resp.available_wake_words.back();
|
||||
resp_wake_word.set_id(StringRef(wake_word.id));
|
||||
resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
|
||||
resp_wake_word.id = StringRef(wake_word.id);
|
||||
resp_wake_word.wake_word = StringRef(wake_word.wake_word);
|
||||
for (const auto &lang : wake_word.trained_languages) {
|
||||
resp_wake_word.trained_languages.push_back(lang);
|
||||
}
|
||||
@@ -1279,8 +1283,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
|
||||
|
||||
resp.available_wake_words.emplace_back();
|
||||
auto &resp_wake_word = resp.available_wake_words.back();
|
||||
resp_wake_word.set_id(StringRef(wake_word.id));
|
||||
resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
|
||||
resp_wake_word.id = StringRef(wake_word.id);
|
||||
resp_wake_word.wake_word = StringRef(wake_word.wake_word);
|
||||
for (const auto &lang : wake_word.trained_languages) {
|
||||
resp_wake_word.trained_languages.push_back(lang);
|
||||
}
|
||||
@@ -1414,14 +1418,15 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void APIConnection::send_event(event::Event *event, const char *event_type) {
|
||||
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||
void APIConnection::send_event(event::Event *event, StringRef event_type) {
|
||||
// get_last_event_type() returns StringRef pointing to null-terminated string literals from codegen
|
||||
this->send_message_smart_(event, MessageCreator(event_type.c_str()), EventResponse::MESSAGE_TYPE,
|
||||
EventResponse::ESTIMATED_SIZE);
|
||||
}
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single) {
|
||||
EventResponse resp;
|
||||
resp.set_event_type(StringRef(event_type));
|
||||
resp.event_type = event_type;
|
||||
return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
|
||||
@@ -1429,13 +1434,42 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
bool is_single) {
|
||||
auto *event = static_cast<event::Event *>(entity);
|
||||
ListEntitiesEventResponse msg;
|
||||
msg.set_device_class(event->get_device_class_ref());
|
||||
msg.device_class = event->get_device_class_ref();
|
||||
msg.event_types = &event->get_event_types();
|
||||
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
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
|
||||
ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared)
|
||||
call.set_carrier_frequency(msg.carrier_frequency);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.perform();
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) {
|
||||
this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single) {
|
||||
auto *infrared = static_cast<infrared::Infrared *>(entity);
|
||||
ListEntitiesInfraredResponse msg;
|
||||
msg.capabilities = infrared->get_capability_flags();
|
||||
return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool APIConnection::send_update_state(update::UpdateEntity *update) {
|
||||
return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE,
|
||||
@@ -1452,11 +1486,11 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
|
||||
resp.has_progress = true;
|
||||
resp.progress = update->update_info.progress;
|
||||
}
|
||||
resp.set_current_version(StringRef(update->update_info.current_version));
|
||||
resp.set_latest_version(StringRef(update->update_info.latest_version));
|
||||
resp.set_title(StringRef(update->update_info.title));
|
||||
resp.set_release_summary(StringRef(update->update_info.summary));
|
||||
resp.set_release_url(StringRef(update->update_info.release_url));
|
||||
resp.current_version = StringRef(update->update_info.current_version);
|
||||
resp.latest_version = StringRef(update->update_info.latest_version);
|
||||
resp.title = StringRef(update->update_info.title);
|
||||
resp.release_summary = StringRef(update->update_info.summary);
|
||||
resp.release_url = StringRef(update->update_info.release_url);
|
||||
}
|
||||
return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -1464,7 +1498,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *update = static_cast<update::UpdateEntity *>(entity);
|
||||
ListEntitiesUpdateResponse msg;
|
||||
msg.set_device_class(update->get_device_class_ref());
|
||||
msg.device_class = update->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -1531,8 +1565,8 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 14;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.set_server_info(ESPHOME_VERSION_REF);
|
||||
resp.set_name(StringRef(App.get_name()));
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
|
||||
// Auto-authenticate - password auth was removed in ESPHome 2026.1.0
|
||||
this->complete_authentication_();
|
||||
@@ -1547,24 +1581,24 @@ bool APIConnection::send_ping_response(const PingRequest &msg) {
|
||||
|
||||
bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
DeviceInfoResponse resp{};
|
||||
resp.set_name(StringRef(App.get_name()));
|
||||
resp.set_friendly_name(StringRef(App.get_friendly_name()));
|
||||
resp.name = StringRef(App.get_name());
|
||||
resp.friendly_name = StringRef(App.get_friendly_name());
|
||||
#ifdef USE_AREAS
|
||||
resp.set_suggested_area(StringRef(App.get_area()));
|
||||
resp.suggested_area = StringRef(App.get_area());
|
||||
#endif
|
||||
// Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
|
||||
char mac_address[18];
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
format_mac_addr_upper(mac, mac_address);
|
||||
resp.set_mac_address(StringRef(mac_address));
|
||||
resp.mac_address = StringRef(mac_address);
|
||||
|
||||
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
||||
resp.esphome_version = ESPHOME_VERSION_REF;
|
||||
|
||||
// Stack buffer for build time string
|
||||
char build_time_str[Application::BUILD_TIME_STR_SIZE];
|
||||
App.get_build_time_string(build_time_str);
|
||||
resp.set_compilation_time(StringRef(build_time_str));
|
||||
resp.compilation_time = StringRef(build_time_str);
|
||||
|
||||
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
@@ -1588,10 +1622,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER;
|
||||
char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)];
|
||||
memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM));
|
||||
resp.set_manufacturer(StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1));
|
||||
resp.manufacturer = StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1);
|
||||
#else
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
|
||||
resp.set_manufacturer(MANUFACTURER);
|
||||
resp.manufacturer = MANUFACTURER;
|
||||
#endif
|
||||
#undef ESPHOME_MANUFACTURER
|
||||
|
||||
@@ -1599,10 +1633,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD;
|
||||
char model_buf[sizeof(MODEL_PROGMEM)];
|
||||
memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM));
|
||||
resp.set_model(StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1));
|
||||
resp.model = StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1);
|
||||
#else
|
||||
static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
|
||||
resp.set_model(MODEL);
|
||||
resp.model = MODEL;
|
||||
#endif
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
|
||||
@@ -1615,13 +1649,13 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)];
|
||||
memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM));
|
||||
memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM));
|
||||
resp.set_project_name(StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1));
|
||||
resp.set_project_version(StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1));
|
||||
resp.project_name = StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1);
|
||||
resp.project_version = StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1);
|
||||
#else
|
||||
static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
|
||||
static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
|
||||
resp.set_project_name(PROJECT_NAME);
|
||||
resp.set_project_version(PROJECT_VERSION);
|
||||
resp.project_name = PROJECT_NAME;
|
||||
resp.project_version = PROJECT_VERSION;
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
@@ -1632,7 +1666,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
// Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
|
||||
char bluetooth_mac[18];
|
||||
bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac);
|
||||
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
|
||||
resp.bluetooth_mac_address = StringRef(bluetooth_mac);
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
|
||||
@@ -1651,7 +1685,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
break;
|
||||
auto &device_info = resp.devices[device_index++];
|
||||
device_info.device_id = device->get_device_id();
|
||||
device_info.set_name(StringRef(device->get_name()));
|
||||
device_info.name = StringRef(device->get_name());
|
||||
device_info.area_id = device->get_area_id();
|
||||
}
|
||||
#endif
|
||||
@@ -1662,7 +1696,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
break;
|
||||
auto &area_info = resp.areas[area_index++];
|
||||
area_info.area_id = area->get_area_id();
|
||||
area_info.set_name(StringRef(area->get_name()));
|
||||
area_info.name = StringRef(area->get_name());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1741,7 +1775,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(error_message);
|
||||
resp.error_message = error_message;
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
@@ -1750,7 +1784,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(error_message);
|
||||
resp.error_message = error_message;
|
||||
resp.response_data = response_data;
|
||||
resp.response_data_len = response_data_len;
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
@@ -1819,10 +1853,30 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
||||
return false;
|
||||
}
|
||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse
|
||||
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
|
||||
if (!this->try_to_clear_buffer(!is_log_message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Toggle Nagle's algorithm based on message type to prevent log messages from
|
||||
// filling the TCP send buffer and crowding out important state updates.
|
||||
//
|
||||
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
|
||||
// message with `option (no_delay) = false;` in api.proto, indicating it should
|
||||
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
|
||||
//
|
||||
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
|
||||
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
|
||||
// (~200ms), buffer filling, or when a state update triggers a flush.
|
||||
//
|
||||
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
|
||||
// for immediate delivery. These are time-sensitive and should not be delayed.
|
||||
//
|
||||
// This must be done proactively BEFORE the buffer fills up - checking buffer
|
||||
// state here would be too late since we'd already be in a degraded state.
|
||||
this->helper_->set_nodelay(!is_log_message);
|
||||
|
||||
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
|
||||
if (err == APIError::WOULD_BLOCK)
|
||||
return false;
|
||||
@@ -2035,7 +2089,7 @@ uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnec
|
||||
// Special case: EventResponse uses const char * pointer
|
||||
if (message_type == EventResponse::MESSAGE_TYPE) {
|
||||
auto *e = static_cast<event::Event *>(entity);
|
||||
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
|
||||
return APIConnection::try_send_event_response(e, StringRef(data_.const_char_ptr), conn, remaining_size, is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2071,10 +2125,10 @@ void APIConnection::process_state_subscriptions_() {
|
||||
|
||||
const auto &it = subs[this->state_subs_at_];
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.set_entity_id(StringRef(it.entity_id));
|
||||
resp.entity_id = StringRef(it.entity_id);
|
||||
|
||||
// Avoid string copy by using the const char* pointer if it exists
|
||||
resp.set_attribute(it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""));
|
||||
resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef("");
|
||||
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
|
||||
@@ -172,8 +172,13 @@ class APIConnection final : public APIServerConnection {
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void send_event(event::Event *event, const char *event_type);
|
||||
void send_event(event::Event *event, StringRef event_type);
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
@@ -317,16 +322,16 @@ class APIConnection final : public APIServerConnection {
|
||||
// Buffer must remain in scope until encode_message_to_buffer is called
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
if (!conn->client_supports_api_version(1, 14)) {
|
||||
msg.set_object_id(entity->get_object_id_to(object_id_buf));
|
||||
msg.object_id = entity->get_object_id_to(object_id_buf);
|
||||
}
|
||||
|
||||
if (entity->has_own_name()) {
|
||||
msg.set_name(entity->get_name());
|
||||
msg.name = entity->get_name();
|
||||
}
|
||||
|
||||
// Set common EntityBase properties
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.set_icon(entity->get_icon_ref());
|
||||
msg.icon = entity->get_icon_ref();
|
||||
#endif
|
||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||
@@ -468,8 +473,12 @@ class APIConnection final : public APIServerConnection {
|
||||
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
#endif
|
||||
|
||||
@@ -120,6 +120,27 @@ class APIFrameHelper {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
|
||||
///
|
||||
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
|
||||
/// state updates low-latency (NODELAY enabled). Without this, many small log
|
||||
/// packets fill the TCP send buffer, crowding out important state updates.
|
||||
///
|
||||
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
|
||||
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
|
||||
///
|
||||
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
|
||||
/// @return true if successful or already in desired state
|
||||
bool set_nodelay(bool enable) {
|
||||
if (this->nodelay_enabled_ == enable)
|
||||
return true;
|
||||
int val = enable ? 1 : 0;
|
||||
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
if (err == 0) {
|
||||
this->nodelay_enabled_ = enable;
|
||||
}
|
||||
return err == 0;
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single operation
|
||||
// messages contains (message_type, offset, length) for each message in the buffer
|
||||
@@ -208,7 +229,10 @@ class APIFrameHelper {
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// 8 bytes total, 0 bytes padding
|
||||
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
|
||||
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
|
||||
// messages to coalesce while keeping state updates low-latency.
|
||||
bool nodelay_enabled_{true};
|
||||
|
||||
// Common initialization for both plaintext and noise protocols
|
||||
APIError init_common_();
|
||||
|
||||
@@ -27,7 +27,6 @@ extend google.protobuf.MessageOptions {
|
||||
extend google.protobuf.FieldOptions {
|
||||
optional string field_ifdef = 1042;
|
||||
optional uint32 fixed_array_size = 50007;
|
||||
optional bool no_zero_copy = 50008 [default=false];
|
||||
optional bool fixed_array_skip_zero = 50009 [default=false];
|
||||
optional string fixed_array_size_define = 50010;
|
||||
optional string fixed_array_with_length_define = 50011;
|
||||
@@ -80,4 +79,15 @@ extend google.protobuf.FieldOptions {
|
||||
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
|
||||
// generates: const light::ColorModeMask *supported_color_modes{};
|
||||
optional string container_pointer_no_template = 50014;
|
||||
|
||||
// packed_buffer: Expose raw packed buffer instead of decoding into container
|
||||
// When set on a packed repeated field, the generated code stores a pointer
|
||||
// to the raw protobuf buffer instead of decoding values. This enables
|
||||
// zero-copy passthrough when the consumer can decode on-demand.
|
||||
// The field must be a packed repeated field (packed=true).
|
||||
// Generates three fields:
|
||||
// - const uint8_t *<field>_data_{nullptr};
|
||||
// - uint16_t <field>_length_{0};
|
||||
// - uint16_t <field>_count_{0};
|
||||
optional bool packed_buffer = 50015 [default=false];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -315,15 +315,12 @@ enum ZWaveProxyRequestType : uint32_t {
|
||||
class InfoResponseProtoMessage : public ProtoMessage {
|
||||
public:
|
||||
~InfoResponseProtoMessage() override = default;
|
||||
StringRef object_id_ref_{};
|
||||
void set_object_id(const StringRef &ref) { this->object_id_ref_ = ref; }
|
||||
StringRef object_id{};
|
||||
uint32_t key{0};
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
StringRef name{};
|
||||
bool disabled_by_default{false};
|
||||
#ifdef USE_ENTITY_ICON
|
||||
StringRef icon_ref_{};
|
||||
void set_icon(const StringRef &ref) { this->icon_ref_ = ref; }
|
||||
StringRef icon{};
|
||||
#endif
|
||||
enums::EntityCategory entity_category{};
|
||||
#ifdef USE_DEVICES
|
||||
@@ -381,10 +378,8 @@ class HelloResponse final : public ProtoMessage {
|
||||
#endif
|
||||
uint32_t api_version_major{0};
|
||||
uint32_t api_version_minor{0};
|
||||
StringRef server_info_ref_{};
|
||||
void set_server_info(const StringRef &ref) { this->server_info_ref_ = ref; }
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
StringRef server_info{};
|
||||
StringRef name{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -462,8 +457,7 @@ class DeviceInfoRequest final : public ProtoMessage {
|
||||
class AreaInfo final : public ProtoMessage {
|
||||
public:
|
||||
uint32_t area_id{0};
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
StringRef name{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -477,8 +471,7 @@ class AreaInfo final : public ProtoMessage {
|
||||
class DeviceInfo final : public ProtoMessage {
|
||||
public:
|
||||
uint32_t device_id{0};
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
StringRef name{};
|
||||
uint32_t area_id{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -496,26 +489,19 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "device_info_response"; }
|
||||
#endif
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
StringRef mac_address_ref_{};
|
||||
void set_mac_address(const StringRef &ref) { this->mac_address_ref_ = ref; }
|
||||
StringRef esphome_version_ref_{};
|
||||
void set_esphome_version(const StringRef &ref) { this->esphome_version_ref_ = ref; }
|
||||
StringRef compilation_time_ref_{};
|
||||
void set_compilation_time(const StringRef &ref) { this->compilation_time_ref_ = ref; }
|
||||
StringRef model_ref_{};
|
||||
void set_model(const StringRef &ref) { this->model_ref_ = ref; }
|
||||
StringRef name{};
|
||||
StringRef mac_address{};
|
||||
StringRef esphome_version{};
|
||||
StringRef compilation_time{};
|
||||
StringRef model{};
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
bool has_deep_sleep{false};
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
StringRef project_name_ref_{};
|
||||
void set_project_name(const StringRef &ref) { this->project_name_ref_ = ref; }
|
||||
StringRef project_name{};
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
StringRef project_version_ref_{};
|
||||
void set_project_version(const StringRef &ref) { this->project_version_ref_ = ref; }
|
||||
StringRef project_version{};
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
uint32_t webserver_port{0};
|
||||
@@ -523,20 +509,16 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
uint32_t bluetooth_proxy_feature_flags{0};
|
||||
#endif
|
||||
StringRef manufacturer_ref_{};
|
||||
void set_manufacturer(const StringRef &ref) { this->manufacturer_ref_ = ref; }
|
||||
StringRef friendly_name_ref_{};
|
||||
void set_friendly_name(const StringRef &ref) { this->friendly_name_ref_ = ref; }
|
||||
StringRef manufacturer{};
|
||||
StringRef friendly_name{};
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
uint32_t voice_assistant_feature_flags{0};
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
StringRef suggested_area_ref_{};
|
||||
void set_suggested_area(const StringRef &ref) { this->suggested_area_ref_ = ref; }
|
||||
StringRef suggested_area{};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
StringRef bluetooth_mac_address_ref_{};
|
||||
void set_bluetooth_mac_address(const StringRef &ref) { this->bluetooth_mac_address_ref_ = ref; }
|
||||
StringRef bluetooth_mac_address{};
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool api_encryption_supported{false};
|
||||
@@ -611,8 +593,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_binary_sensor_response"; }
|
||||
#endif
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
bool is_status_binary_sensor{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -651,8 +632,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
|
||||
bool assumed_state{false};
|
||||
bool supports_position{false};
|
||||
bool supports_tilt{false};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
bool supports_stop{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -733,8 +713,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
|
||||
bool oscillating{false};
|
||||
enums::FanDirection direction{};
|
||||
int32_t speed_level{0};
|
||||
StringRef preset_mode_ref_{};
|
||||
void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; }
|
||||
StringRef preset_mode{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -808,8 +787,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
|
||||
float color_temperature{0.0f};
|
||||
float cold_white{0.0f};
|
||||
float warm_white{0.0f};
|
||||
StringRef effect_ref_{};
|
||||
void set_effect(const StringRef &ref) { this->effect_ref_ = ref; }
|
||||
StringRef effect{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -869,12 +847,10 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_sensor_response"; }
|
||||
#endif
|
||||
StringRef unit_of_measurement_ref_{};
|
||||
void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; }
|
||||
StringRef unit_of_measurement{};
|
||||
int32_t accuracy_decimals{0};
|
||||
bool force_update{false};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
enums::SensorStateClass state_class{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -911,8 +887,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage {
|
||||
const char *message_name() const override { return "list_entities_switch_response"; }
|
||||
#endif
|
||||
bool assumed_state{false};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -962,8 +937,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_text_sensor_response"; }
|
||||
#endif
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -979,8 +953,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "text_sensor_state_response"; }
|
||||
#endif
|
||||
StringRef state_ref_{};
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
StringRef state{};
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -1079,9 +1052,8 @@ class SubscribeHomeassistantServicesRequest final : public ProtoMessage {
|
||||
};
|
||||
class HomeassistantServiceMap final : public ProtoMessage {
|
||||
public:
|
||||
StringRef key_ref_{};
|
||||
void set_key(const StringRef &ref) { this->key_ref_ = ref; }
|
||||
std::string value{};
|
||||
StringRef key{};
|
||||
StringRef value{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1097,8 +1069,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "homeassistant_action_request"; }
|
||||
#endif
|
||||
StringRef service_ref_{};
|
||||
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
||||
StringRef service{};
|
||||
FixedVector<HomeassistantServiceMap> data{};
|
||||
FixedVector<HomeassistantServiceMap> data_template{};
|
||||
FixedVector<HomeassistantServiceMap> variables{};
|
||||
@@ -1110,7 +1081,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
||||
bool wants_response{false};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
std::string response_template{};
|
||||
StringRef response_template{};
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -1166,10 +1137,8 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_home_assistant_state_response"; }
|
||||
#endif
|
||||
StringRef entity_id_ref_{};
|
||||
void set_entity_id(const StringRef &ref) { this->entity_id_ref_ = ref; }
|
||||
StringRef attribute_ref_{};
|
||||
void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; }
|
||||
StringRef entity_id{};
|
||||
StringRef attribute{};
|
||||
bool once{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -1230,8 +1199,7 @@ class GetTimeResponse final : public ProtoDecodableMessage {
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
public:
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
StringRef name{};
|
||||
enums::ServiceArgType type{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -1248,8 +1216,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_services_response"; }
|
||||
#endif
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
StringRef name{};
|
||||
uint32_t key{0};
|
||||
FixedVector<ListEntitiesServicesArgument> args{};
|
||||
enums::SupportsResponseType supports_response{};
|
||||
@@ -1318,8 +1285,7 @@ class ExecuteServiceResponse final : public ProtoMessage {
|
||||
#endif
|
||||
uint32_t call_id{0};
|
||||
bool success{false};
|
||||
StringRef error_message_ref_{};
|
||||
void set_error_message(const StringRef &ref) { this->error_message_ref_ = ref; }
|
||||
StringRef error_message{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
const uint8_t *response_data{nullptr};
|
||||
uint16_t response_data_len{0};
|
||||
@@ -1437,11 +1403,9 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
|
||||
enums::ClimateAction action{};
|
||||
enums::ClimateFanMode fan_mode{};
|
||||
enums::ClimateSwingMode swing_mode{};
|
||||
StringRef custom_fan_mode_ref_{};
|
||||
void set_custom_fan_mode(const StringRef &ref) { this->custom_fan_mode_ref_ = ref; }
|
||||
StringRef custom_fan_mode{};
|
||||
enums::ClimatePreset preset{};
|
||||
StringRef custom_preset_ref_{};
|
||||
void set_custom_preset(const StringRef &ref) { this->custom_preset_ref_ = ref; }
|
||||
StringRef custom_preset{};
|
||||
float current_humidity{0.0f};
|
||||
float target_humidity{0.0f};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
@@ -1564,11 +1528,9 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
|
||||
float min_value{0.0f};
|
||||
float max_value{0.0f};
|
||||
float step{0.0f};
|
||||
StringRef unit_of_measurement_ref_{};
|
||||
void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; }
|
||||
StringRef unit_of_measurement{};
|
||||
enums::NumberMode mode{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1635,8 +1597,7 @@ class SelectStateResponse final : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "select_state_response"; }
|
||||
#endif
|
||||
StringRef state_ref_{};
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
StringRef state{};
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -1735,8 +1696,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage {
|
||||
bool assumed_state{false};
|
||||
bool supports_open{false};
|
||||
bool requires_code{false};
|
||||
StringRef code_format_ref_{};
|
||||
void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; }
|
||||
StringRef code_format{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1789,8 +1749,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_button_response"; }
|
||||
#endif
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1818,8 +1777,7 @@ class ButtonCommandRequest final : public CommandProtoMessage {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
class MediaPlayerSupportedFormat final : public ProtoMessage {
|
||||
public:
|
||||
StringRef format_ref_{};
|
||||
void set_format(const StringRef &ref) { this->format_ref_ = ref; }
|
||||
StringRef format{};
|
||||
uint32_t sample_rate{0};
|
||||
uint32_t num_channels{0};
|
||||
enums::MediaPlayerFormatPurpose purpose{};
|
||||
@@ -2424,12 +2382,10 @@ class VoiceAssistantRequest final : public ProtoMessage {
|
||||
const char *message_name() const override { return "voice_assistant_request"; }
|
||||
#endif
|
||||
bool start{false};
|
||||
StringRef conversation_id_ref_{};
|
||||
void set_conversation_id(const StringRef &ref) { this->conversation_id_ref_ = ref; }
|
||||
StringRef conversation_id{};
|
||||
uint32_t flags{0};
|
||||
VoiceAssistantAudioSettings audio_settings{};
|
||||
StringRef wake_word_phrase_ref_{};
|
||||
void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; }
|
||||
StringRef wake_word_phrase{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -2560,10 +2516,8 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage {
|
||||
};
|
||||
class VoiceAssistantWakeWord final : public ProtoMessage {
|
||||
public:
|
||||
StringRef id_ref_{};
|
||||
void set_id(const StringRef &ref) { this->id_ref_ = ref; }
|
||||
StringRef wake_word_ref_{};
|
||||
void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; }
|
||||
StringRef id{};
|
||||
StringRef wake_word{};
|
||||
std::vector<std::string> trained_languages{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -2703,8 +2657,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage {
|
||||
#endif
|
||||
uint32_t min_length{0};
|
||||
uint32_t max_length{0};
|
||||
StringRef pattern_ref_{};
|
||||
void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; }
|
||||
StringRef pattern{};
|
||||
enums::TextMode mode{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -2721,8 +2674,7 @@ class TextStateResponse final : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "text_state_response"; }
|
||||
#endif
|
||||
StringRef state_ref_{};
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
StringRef state{};
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -2866,8 +2818,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_event_response"; }
|
||||
#endif
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
const FixedVector<const char *> *event_types{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -2884,8 +2835,7 @@ class EventResponse final : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "event_response"; }
|
||||
#endif
|
||||
StringRef event_type_ref_{};
|
||||
void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; }
|
||||
StringRef event_type{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -2903,8 +2853,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_valve_response"; }
|
||||
#endif
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
bool assumed_state{false};
|
||||
bool supports_position{false};
|
||||
bool supports_stop{false};
|
||||
@@ -3010,8 +2959,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_update_response"; }
|
||||
#endif
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
StringRef device_class{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -3031,16 +2979,11 @@ class UpdateStateResponse final : public StateResponseProtoMessage {
|
||||
bool in_progress{false};
|
||||
bool has_progress{false};
|
||||
float progress{0.0f};
|
||||
StringRef current_version_ref_{};
|
||||
void set_current_version(const StringRef &ref) { this->current_version_ref_ = ref; }
|
||||
StringRef latest_version_ref_{};
|
||||
void set_latest_version(const StringRef &ref) { this->latest_version_ref_ = ref; }
|
||||
StringRef title_ref_{};
|
||||
void set_title(const StringRef &ref) { this->title_ref_ = ref; }
|
||||
StringRef release_summary_ref_{};
|
||||
void set_release_summary(const StringRef &ref) { this->release_summary_ref_ = ref; }
|
||||
StringRef release_url_ref_{};
|
||||
void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; }
|
||||
StringRef current_version{};
|
||||
StringRef latest_version{};
|
||||
StringRef title{};
|
||||
StringRef release_summary{};
|
||||
StringRef release_url{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -3106,5 +3049,70 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage {
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 135;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 44;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_infrared_response"; }
|
||||
#endif
|
||||
uint32_t capabilities{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 136;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 220;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "infrared_rf_transmit_raw_timings_request"; }
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
uint32_t key{0};
|
||||
uint32_t carrier_frequency{0};
|
||||
uint32_t repeat_count{0};
|
||||
const uint8_t *timings_data_{nullptr};
|
||||
uint16_t timings_length_{0};
|
||||
uint16_t timings_count_{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class InfraredRFReceiveEvent final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 137;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "infrared_rf_receive_event"; }
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
uint32_t key{0};
|
||||
const std::vector<int32_t> *timings{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -621,6 +621,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_water_heater_command_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
|
||||
InfraredRFTransmitRawTimingsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_infrared_rf_transmit_raw_timings_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_infrared_rf_transmit_raw_timings_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
@@ -819,6 +830,11 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th
|
||||
#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
|
||||
|
||||
@@ -217,6 +217,11 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
@@ -347,6 +352,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#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;
|
||||
@@ -473,6 +481,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#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;
|
||||
};
|
||||
|
||||
@@ -186,14 +186,17 @@ void APIServer::loop() {
|
||||
}
|
||||
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername()));
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before removal for the trigger
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername());
|
||||
#endif
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
@@ -205,6 +208,11 @@ void APIServer::loop() {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_->trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
}
|
||||
@@ -339,6 +347,21 @@ void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
const std::vector<int32_t> *timings) {
|
||||
InfraredRFReceiveEvent resp{};
|
||||
#ifdef USE_DEVICES
|
||||
resp.device_id = device_id;
|
||||
#endif
|
||||
resp.key = key;
|
||||
resp.timings = timings;
|
||||
|
||||
for (auto &c : this->clients_)
|
||||
c->send_infrared_rf_receive_event(resp);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
|
||||
#endif
|
||||
@@ -616,6 +639,14 @@ bool APIServer::teardown() {
|
||||
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
|
||||
#endif
|
||||
|
||||
// SSO-friendly action call key - hex format guarantees max 11 chars ("ac_ffffffff")
|
||||
// which fits in any std::string SSO buffer (typically 12-15 bytes)
|
||||
static inline std::string make_action_call_key(uint32_t id) {
|
||||
char buf[12];
|
||||
size_t len = snprintf(buf, sizeof(buf), "ac_%x", id);
|
||||
return std::string(buf, len);
|
||||
}
|
||||
|
||||
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
|
||||
uint32_t action_call_id = this->next_action_call_id_++;
|
||||
// Handle wraparound (skip 0 as it means "no call")
|
||||
@@ -625,18 +656,17 @@ uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConn
|
||||
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
|
||||
|
||||
// Schedule automatic cleanup after timeout (client will have given up by then)
|
||||
this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS,
|
||||
[this, action_call_id]() {
|
||||
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
|
||||
this->unregister_active_action_call(action_call_id);
|
||||
});
|
||||
this->set_timeout(make_action_call_key(action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
|
||||
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
|
||||
this->unregister_active_action_call(action_call_id);
|
||||
});
|
||||
|
||||
return action_call_id;
|
||||
}
|
||||
|
||||
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
|
||||
// Cancel the timeout for this action call
|
||||
this->cancel_timeout(str_sprintf("action_call_%u", action_call_id));
|
||||
this->cancel_timeout(make_action_call_key(action_call_id));
|
||||
|
||||
// Swap-and-pop is more efficient than remove_if for unordered vectors
|
||||
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
|
||||
@@ -653,7 +683,7 @@ void APIServer::unregister_active_action_calls_for_connection(APIConnection *con
|
||||
for (size_t i = 0; i < this->active_action_calls_.size();) {
|
||||
if (this->active_action_calls_[i].connection == conn) {
|
||||
// Cancel the timeout for this action call
|
||||
this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id));
|
||||
this->cancel_timeout(make_action_call_key(this->active_action_calls_[i].action_call_id));
|
||||
|
||||
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
||||
this->active_action_calls_.pop_back();
|
||||
|
||||
@@ -185,6 +185,9 @@ class APIServer : public Component,
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
bool is_connected(bool state_subscription_only = false) const;
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -260,12 +260,12 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
kv.key = StringRef(it.first);
|
||||
kv.value = StringRef(it.second); // data map lives until send completes
|
||||
}
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
@@ -282,7 +282,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &event_name) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(event_name));
|
||||
resp.service = StringRef(event_name);
|
||||
resp.is_event = true;
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
@@ -302,13 +302,13 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
resp.is_event = true;
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
kv.key = StringRef(it.first);
|
||||
kv.value = StringRef(it.second); // data map lives until send completes
|
||||
}
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -147,13 +147,23 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
void play(const Ts &...x) override {
|
||||
HomeassistantActionRequest resp;
|
||||
std::string service_value = this->service_.value(x...);
|
||||
resp.set_service(StringRef(service_value));
|
||||
resp.service = StringRef(service_value);
|
||||
resp.is_event = this->flags_.is_event;
|
||||
this->populate_service_map(resp.data, this->data_, x...);
|
||||
this->populate_service_map(resp.data_template, this->data_template_, x...);
|
||||
this->populate_service_map(resp.variables, this->variables_, x...);
|
||||
|
||||
// Local storage for lambda-evaluated strings - lives until after send
|
||||
FixedVector<std::string> data_storage;
|
||||
FixedVector<std::string> data_template_storage;
|
||||
FixedVector<std::string> variables_storage;
|
||||
|
||||
this->populate_service_map(resp.data, this->data_, data_storage, x...);
|
||||
this->populate_service_map(resp.data_template, this->data_template_, data_template_storage, x...);
|
||||
this->populate_service_map(resp.variables, this->variables_, variables_storage, x...);
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
// IMPORTANT: Declare at outer scope so it lives until send_homeassistant_action returns.
|
||||
std::string response_template_value;
|
||||
#endif
|
||||
if (this->flags_.wants_status) {
|
||||
// Generate a unique call ID for this service call
|
||||
static uint32_t call_id_counter = 1;
|
||||
@@ -164,8 +174,8 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
resp.wants_response = true;
|
||||
// Set response template if provided
|
||||
if (this->flags_.has_response_template) {
|
||||
std::string response_template_value = this->response_template_.value(x...);
|
||||
resp.response_template = response_template_value;
|
||||
response_template_value = this->response_template_.value(x...);
|
||||
resp.response_template = StringRef(response_template_value);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -205,12 +215,31 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
}
|
||||
|
||||
template<typename VectorType, typename SourceType>
|
||||
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
|
||||
static void populate_service_map(VectorType &dest, SourceType &source, FixedVector<std::string> &value_storage,
|
||||
Ts... x) {
|
||||
dest.init(source.size());
|
||||
|
||||
// Count non-static strings to allocate exact storage needed
|
||||
size_t lambda_count = 0;
|
||||
for (const auto &it : source) {
|
||||
if (!it.value.is_static_string()) {
|
||||
lambda_count++;
|
||||
}
|
||||
}
|
||||
value_storage.init(lambda_count);
|
||||
|
||||
for (auto &it : source) {
|
||||
auto &kv = dest.emplace_back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
kv.key = StringRef(it.key);
|
||||
|
||||
if (it.value.is_static_string()) {
|
||||
// Static string from YAML - zero allocation
|
||||
kv.value = StringRef(it.value.get_static_string());
|
||||
} else {
|
||||
// Lambda evaluation - store result, reference it
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,9 @@ LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPane
|
||||
#ifdef USE_WATER_HEATER
|
||||
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse)
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
|
||||
#endif
|
||||
|
||||
@@ -85,6 +85,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool on_water_heater(water_heater::WaterHeater *entity) override;
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
bool on_infrared(infrared::Infrared *entity) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *entity) override;
|
||||
#endif
|
||||
|
||||
@@ -39,6 +39,24 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
|
||||
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
|
||||
}
|
||||
|
||||
/// Count number of varints in a packed buffer
|
||||
inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
|
||||
uint16_t count = 0;
|
||||
while (len > 0) {
|
||||
// Skip varint bytes until we find one without continuation bit
|
||||
while (len > 0 && (*data & 0x80)) {
|
||||
data++;
|
||||
len--;
|
||||
}
|
||||
if (len > 0) {
|
||||
data++;
|
||||
len--;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/*
|
||||
* StringRef Ownership Model for API Protocol Messages
|
||||
* ===================================================
|
||||
@@ -54,16 +72,16 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
|
||||
* 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe
|
||||
* 4. Local variables: Safe ONLY if encoding happens before function returns:
|
||||
* std::string temp = compute_value();
|
||||
* msg.set_field(StringRef(temp));
|
||||
* msg.field = StringRef(temp);
|
||||
* return this->send_message(msg); // temp is valid during encoding
|
||||
*
|
||||
* Unsafe Patterns (WILL cause crashes/corruption):
|
||||
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
|
||||
* 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
|
||||
* 1. Temporaries: msg.field = StringRef(obj.get_string()) // get_string() returns by value
|
||||
* 2. Concatenation: msg.field = StringRef(str1 + str2) // Result is temporary
|
||||
*
|
||||
* For unsafe patterns, store in a local variable first:
|
||||
* std::string temp = get_string(); // or str1 + str2
|
||||
* msg.set_field(StringRef(temp));
|
||||
* msg.field = StringRef(temp);
|
||||
*
|
||||
* The send_*_response pattern ensures proper lifetime management by encoding
|
||||
* within the same function scope where temporaries are created.
|
||||
@@ -180,9 +198,10 @@ class ProtoVarInt {
|
||||
uint64_t value_;
|
||||
};
|
||||
|
||||
// Forward declaration for decode_to_message and encode_to_writer
|
||||
class ProtoMessage;
|
||||
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
|
||||
class ProtoDecodableMessage;
|
||||
class ProtoMessage;
|
||||
class ProtoSize;
|
||||
|
||||
class ProtoLengthDelimited {
|
||||
public:
|
||||
@@ -334,6 +353,8 @@ class ProtoWriteBuffer {
|
||||
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
|
||||
this->encode_uint64(field_id, encode_zigzag64(value), force);
|
||||
}
|
||||
/// Encode a packed repeated sint32 field (zero-copy from vector)
|
||||
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
|
||||
void encode_message(uint32_t field_id, const ProtoMessage &value);
|
||||
std::vector<uint8_t> *get_buffer() const { return buffer_; }
|
||||
|
||||
@@ -341,9 +362,6 @@ class ProtoWriteBuffer {
|
||||
std::vector<uint8_t> *buffer_;
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class ProtoSize;
|
||||
|
||||
class ProtoMessage {
|
||||
public:
|
||||
virtual ~ProtoMessage() = default;
|
||||
@@ -792,8 +810,43 @@ class ProtoSize {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculate size of a packed repeated sint32 field
|
||||
*/
|
||||
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
|
||||
if (values.empty())
|
||||
return;
|
||||
|
||||
size_t packed_size = 0;
|
||||
for (int value : values) {
|
||||
packed_size += varint(encode_zigzag32(value));
|
||||
}
|
||||
|
||||
// field_id + length varint + packed data
|
||||
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
|
||||
}
|
||||
};
|
||||
|
||||
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
|
||||
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
|
||||
if (values.empty())
|
||||
return;
|
||||
|
||||
// Calculate packed size
|
||||
size_t packed_size = 0;
|
||||
for (int value : values) {
|
||||
packed_size += ProtoSize::varint(encode_zigzag32(value));
|
||||
}
|
||||
|
||||
// Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values
|
||||
this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED);
|
||||
this->encode_varint_raw(packed_size);
|
||||
for (int value : values) {
|
||||
this->encode_varint_raw(encode_zigzag32(value));
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of encode_message - must be after ProtoMessage is defined
|
||||
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
|
||||
|
||||
@@ -79,6 +79,9 @@ class InitialStateIterator : public ComponentIterator {
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool on_water_heater(water_heater::WaterHeater *entity) override;
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
bool on_infrared(infrared::Infrared *infrared) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override { return true; };
|
||||
#endif
|
||||
|
||||
@@ -46,7 +46,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.name = StringRef(this->name_);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = this->supports_response_;
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
@@ -54,7 +54,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.set_name(StringRef(this->arg_names_[i]));
|
||||
arg.name = StringRef(this->arg_names_[i]);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.name = StringRef(this->name_);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
@@ -116,7 +116,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.set_name(StringRef(this->arg_names_[i]));
|
||||
arg.name = StringRef(this->arg_names_[i]);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::aqi {
|
||||
|
||||
class AbstractAQICalculator {
|
||||
public:
|
||||
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
|
||||
virtual uint16_t get_aqi(float pm2_5_value, float pm10_0_value) = 0;
|
||||
};
|
||||
|
||||
} // namespace esphome::aqi
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
|
||||
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
|
||||
@@ -9,11 +10,11 @@ namespace esphome::aqi {
|
||||
|
||||
class AQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
|
||||
int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index;
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -21,25 +22,28 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
|
||||
|
||||
static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, {56, 125}, {126, 225}, {226, INT_MAX}};
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
|
||||
{35.5f, 55.4f}, {55.5f, 125.4f},
|
||||
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
|
||||
|
||||
static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254},
|
||||
{255, 354}, {355, 424}, {425, INT_MAX}};
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
|
||||
{155.0f, 254.0f}, {255.0f, 354.0f},
|
||||
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
|
||||
|
||||
static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) {
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
if (grid_index == -1) {
|
||||
return -1;
|
||||
return -1.0f;
|
||||
}
|
||||
int aqi_lo = INDEX_GRID[grid_index][0];
|
||||
int aqi_hi = INDEX_GRID[grid_index][1];
|
||||
int conc_lo = array[grid_index][0];
|
||||
int conc_hi = array[grid_index][1];
|
||||
float aqi_lo = INDEX_GRID[grid_index][0];
|
||||
float aqi_hi = INDEX_GRID[grid_index][1];
|
||||
float conc_lo = array[grid_index][0];
|
||||
float conc_hi = array[grid_index][1];
|
||||
|
||||
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
|
||||
}
|
||||
|
||||
static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) {
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
return i;
|
||||
|
||||
@@ -44,8 +44,7 @@ void AQISensor::calculate_aqi_() {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t aqi =
|
||||
calculator->get_aqi(static_cast<uint16_t>(this->pm_2_5_value_), static_cast<uint16_t>(this->pm_10_0_value_));
|
||||
uint16_t aqi = calculator->get_aqi(this->pm_2_5_value_, this->pm_10_0_value_);
|
||||
this->publish_state(aqi);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
|
||||
namespace esphome::aqi {
|
||||
|
||||
class CAQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
|
||||
int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index;
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -18,25 +20,27 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
||||
|
||||
static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}};
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
|
||||
|
||||
static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}};
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
|
||||
|
||||
static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) {
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
if (grid_index == -1) {
|
||||
return -1;
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
int aqi_lo = INDEX_GRID[grid_index][0];
|
||||
int aqi_hi = INDEX_GRID[grid_index][1];
|
||||
int conc_lo = array[grid_index][0];
|
||||
int conc_hi = array[grid_index][1];
|
||||
float aqi_lo = INDEX_GRID[grid_index][0];
|
||||
float aqi_hi = INDEX_GRID[grid_index][1];
|
||||
float conc_lo = array[grid_index][0];
|
||||
float conc_hi = array[grid_index][1];
|
||||
|
||||
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
|
||||
}
|
||||
|
||||
static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) {
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
return i;
|
||||
|
||||
@@ -164,21 +164,21 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
return;
|
||||
}
|
||||
} else if (call.has_custom_preset()) {
|
||||
const char *preset = call.get_custom_preset();
|
||||
auto preset = call.get_custom_preset();
|
||||
bool result;
|
||||
|
||||
if (strcmp(preset, "M1") == 0) {
|
||||
if (preset == "M1") {
|
||||
result = this->parent_->button_memory1();
|
||||
} else if (strcmp(preset, "M2") == 0) {
|
||||
} else if (preset == "M2") {
|
||||
result = this->parent_->button_memory2();
|
||||
} else if (strcmp(preset, "M3") == 0) {
|
||||
} else if (preset == "M3") {
|
||||
result = this->parent_->button_memory3();
|
||||
} else if (strcmp(preset, "LTD HT") == 0) {
|
||||
} else if (preset == "LTD HT") {
|
||||
result = this->parent_->button_heat();
|
||||
} else if (strcmp(preset, "EXT HT") == 0) {
|
||||
} else if (preset == "EXT HT") {
|
||||
result = this->parent_->button_ext_heat();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unsupported preset: %s", preset);
|
||||
ESP_LOGW(TAG, "Unsupported preset: %.*s", (int) preset.size(), preset.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -208,10 +208,11 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
this->set_fan_mode_(fan_mode);
|
||||
}
|
||||
} else if (call.has_custom_fan_mode()) {
|
||||
const char *fan_mode = call.get_custom_fan_mode();
|
||||
auto fan_index = bedjet_fan_speed_to_step(fan_mode);
|
||||
auto fan_mode = call.get_custom_fan_mode();
|
||||
auto fan_index = bedjet_fan_speed_to_step(fan_mode.c_str());
|
||||
if (fan_index <= 19) {
|
||||
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index);
|
||||
ESP_LOGV(TAG, "[%s] Converted fan mode %.*s to bedjet fan step %d", this->get_name().c_str(),
|
||||
(int) fan_mode.size(), fan_mode.c_str(), fan_index);
|
||||
bool result = this->parent_->set_fan_index(fan_index);
|
||||
if (result) {
|
||||
this->set_custom_fan_mode_(fan_mode);
|
||||
|
||||
@@ -21,7 +21,7 @@ class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSe
|
||||
if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() ||
|
||||
param->notify.handle != this->sensor_->handle)
|
||||
break;
|
||||
this->trigger(this->sensor_->parse_data(param->notify.value, param->notify.value_len));
|
||||
this->trigger(std::string(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len));
|
||||
}
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -11,8 +11,6 @@ namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_text_sensor";
|
||||
|
||||
static const std::string EMPTY = "";
|
||||
|
||||
void BLETextSensor::loop() {
|
||||
// Parent BLEClientNode has a loop() method, but this component uses
|
||||
// polling via update() and BLE callbacks so loop isn't needed
|
||||
@@ -47,7 +45,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
this->publish_state("");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
@@ -55,7 +53,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
this->publish_state("");
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf),
|
||||
@@ -67,7 +65,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
auto *descr = chr->get_descriptor(this->descr_uuid_);
|
||||
if (descr == nullptr) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
this->publish_state("");
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
char descr_buf[esp32_ble::UUID_STR_LEN];
|
||||
@@ -99,7 +97,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
this->publish_state(this->parse_data(param->read.value, param->read.value_len));
|
||||
this->publish_state(reinterpret_cast<const char *>(param->read.value), param->read.value_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -108,7 +106,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
this->publish_state(this->parse_data(param->notify.value, param->notify.value_len));
|
||||
this->publish_state(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
@@ -121,11 +119,6 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
}
|
||||
|
||||
std::string BLETextSensor::parse_data(uint8_t *value, uint16_t value_len) {
|
||||
std::string text(value, value + value_len);
|
||||
return text;
|
||||
}
|
||||
|
||||
void BLETextSensor::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str());
|
||||
@@ -140,7 +133,7 @@ void BLETextSensor::update() {
|
||||
ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
this->publish_state("");
|
||||
ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
|
||||
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
void set_enable_notify(bool notify) { this->notify_ = notify; }
|
||||
std::string parse_data(uint8_t *value, uint16_t value_len);
|
||||
uint16_t handle;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -31,7 +31,11 @@ void BME68xBSEC2I2CComponent::dump_config() {
|
||||
BME68xBSEC2Component::dump_config();
|
||||
}
|
||||
|
||||
uint32_t BME68xBSEC2I2CComponent::get_hash() { return fnv1_hash("bme68x_bsec_state_" + to_string(this->address_)); }
|
||||
uint32_t BME68xBSEC2I2CComponent::get_hash() {
|
||||
char buf[22]; // "bme68x_bsec_state_" (18) + uint8_t max (3) + null
|
||||
snprintf(buf, sizeof(buf), "bme68x_bsec_state_%u", this->address_);
|
||||
return fnv1_hash(buf);
|
||||
}
|
||||
|
||||
int8_t BME68xBSEC2I2CComponent::read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr) {
|
||||
ESP_LOGVV(TAG, "read_bytes_wrapper: reg = %u", a_register);
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
CONF_COMPRESSION,
|
||||
CONF_ID,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
@@ -43,6 +44,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
|
||||
web_server_base.WebServerBase
|
||||
),
|
||||
cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on(
|
||||
@@ -96,6 +98,9 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
cg.add_define("USE_CAPTIVE_PORTAL")
|
||||
|
||||
if config[CONF_COMPRESSION] == "gzip":
|
||||
cg.add_define("USE_CAPTIVE_PORTAL_GZIP")
|
||||
|
||||
if CORE.using_arduino:
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("DNSServer", None)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace captive_portal {
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
#ifdef USE_CAPTIVE_PORTAL_GZIP
|
||||
const uint8_t INDEX_GZ[] PROGMEM = {
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e,
|
||||
0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36,
|
||||
@@ -85,5 +85,71 @@ const uint8_t INDEX_GZ[] PROGMEM = {
|
||||
0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68,
|
||||
0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00};
|
||||
|
||||
} // namespace captive_portal
|
||||
} // namespace esphome
|
||||
#else // Brotli (default, smaller)
|
||||
const uint8_t INDEX_BR[] PROGMEM = {
|
||||
0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b,
|
||||
0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48,
|
||||
0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78,
|
||||
0xa5, 0x89, 0x25, 0xb3, 0xda, 0x2c, 0xa2, 0x32, 0x9c, 0x57, 0x07, 0x56, 0xbc, 0x34, 0x13, 0xff, 0x5c, 0x0a, 0xa1,
|
||||
0x67, 0x82, 0xb8, 0x6b, 0x4c, 0x76, 0x31, 0x6c, 0xe3, 0x40, 0x46, 0xea, 0xb0, 0xd4, 0xf4, 0x3b, 0x02, 0x65, 0x18,
|
||||
0xa4, 0xaf, 0xac, 0x6d, 0x55, 0xd6, 0xbe, 0x59, 0x66, 0x7a, 0x7c, 0x60, 0xb2, 0x83, 0x33, 0x23, 0xc9, 0x79, 0x82,
|
||||
0x47, 0xb4, 0x28, 0xf4, 0x24, 0xb5, 0x23, 0x5a, 0x44, 0xe1, 0xc3, 0x27, 0x04, 0xe8, 0x0c, 0xdd, 0xb4, 0xd0, 0x8c,
|
||||
0xfb, 0x10, 0x39, 0x93, 0x04, 0x2a, 0x66, 0x18, 0x4b, 0x74, 0xca, 0x31, 0x7f, 0xb2, 0xe5, 0x45, 0xc1, 0xdd, 0x72,
|
||||
0x49, 0xff, 0x0e, 0xb3, 0xf0, 0x93, 0x18, 0xab, 0x68, 0xad, 0xe1, 0x9d, 0xe4, 0x29, 0xc0, 0xe3, 0x63, 0x54, 0x61,
|
||||
0x1b, 0x45, 0xb9, 0x6c, 0x23, 0x0f, 0x99, 0x7f, 0x8e, 0x69, 0xaa, 0xc1, 0xb8, 0x4e, 0x42, 0x9c, 0xc5, 0x6e, 0x69,
|
||||
0x40, 0x0e, 0x4f, 0x97, 0xd3, 0x23, 0x18, 0xf5, 0xc8, 0x75, 0x73, 0xb5, 0xbd, 0x46, 0x8a, 0x97, 0x7d, 0x83, 0xe4,
|
||||
0x29, 0x72, 0x73, 0xc1, 0x39, 0x8e, 0x7e, 0x84, 0x39, 0x66, 0x57, 0xc6, 0x85, 0x19, 0x8b, 0xf2, 0x4d, 0xd9, 0xfe,
|
||||
0x75, 0xa9, 0xe1, 0x2b, 0x21, 0x81, 0x58, 0x51, 0x99, 0xbc, 0xa4, 0x0b, 0x10, 0x6f, 0x86, 0x17, 0x0b, 0x92, 0x00,
|
||||
0x11, 0x6f, 0x3b, 0xa4, 0xa4, 0x11, 0x7e, 0x0b, 0x97, 0x85, 0x23, 0x0c, 0x01, 0x6f, 0x2a, 0x18, 0xc6, 0xbe, 0x3d,
|
||||
0x77, 0x1a, 0xe6, 0x00, 0x5c, 0x1a, 0x14, 0x47, 0xc6, 0xcc, 0xcc, 0x52, 0xbe, 0x04, 0x19, 0x31, 0x05, 0x46, 0xa0,
|
||||
0xc3, 0x69, 0x0c, 0x60, 0xb7, 0x14, 0x57, 0xa0, 0x92, 0xbf, 0xb7, 0x0c, 0xd8, 0x3a, 0x79, 0x09, 0x99, 0xc9, 0x71,
|
||||
0x88, 0x01, 0x8b, 0xa5, 0x61, 0x0a, 0xb5, 0xe8, 0xc7, 0x71, 0xe7, 0x70, 0x79, 0xb6, 0xe4, 0x01, 0xfc, 0x1a, 0x4a,
|
||||
0x7b, 0x60, 0x6e, 0xef, 0x95, 0x62, 0x59, 0x28, 0xb5, 0x25, 0x56, 0x15, 0xe7, 0xca, 0xad, 0x32, 0xe6, 0xf7, 0x01,
|
||||
0x31, 0x34, 0x87, 0x93, 0x0b, 0x9b, 0x9d, 0x26, 0xff, 0xe5, 0x92, 0xad, 0x6f, 0xb8, 0x3b, 0x16, 0xc1, 0xa0, 0x5a,
|
||||
0x4f, 0x52, 0x0b, 0x2b, 0xc1, 0xa7, 0x95, 0x7b, 0x24, 0x51, 0xd3, 0xb3, 0x23, 0x62, 0x0b, 0xcc, 0xa0, 0x58, 0xa7,
|
||||
0x64, 0x45, 0x2f, 0x0b, 0xdd, 0x1d, 0x97, 0x82, 0x1f, 0xcc, 0x64, 0xdb, 0xd3, 0xf4, 0xb0, 0x8b, 0xc8, 0xcf, 0x15,
|
||||
0x81, 0x8b, 0xa1, 0x9d, 0xf8, 0xfc, 0xec, 0x49, 0x40, 0x12, 0x01, 0x09, 0x51, 0xf3, 0x73, 0x18, 0x24, 0x97, 0x55,
|
||||
0x85, 0x6a, 0x92, 0x1a, 0xf5, 0x5a, 0x05, 0x54, 0x1f, 0x27, 0x0a, 0xa8, 0xa1, 0x94, 0x58, 0x78, 0x7d, 0x87, 0xa8,
|
||||
0xdb, 0x13, 0x66, 0x20, 0x5e, 0x43, 0x18, 0x7a, 0xbb, 0x16, 0x16, 0x07, 0xc8, 0xab, 0x10, 0xe2, 0x50, 0xb9, 0xb1,
|
||||
0xd8, 0x21, 0xc8, 0x4a, 0x2e, 0x99, 0x0e, 0x23, 0x52, 0xc6, 0xcb, 0x29, 0x84, 0x91, 0x03, 0xb1, 0xe2, 0x4c, 0x1d,
|
||||
0x22, 0xd3, 0xc8, 0x79, 0x00, 0x8b, 0x8b, 0x88, 0x1e, 0x29, 0xd3, 0xae, 0x10, 0x15, 0x22, 0x6d, 0xb0, 0x87, 0x6f,
|
||||
0x27, 0x2e, 0x7c, 0xc2, 0x7a, 0x61, 0xbd, 0x22, 0xe5, 0x5f, 0xdd, 0x7b, 0x00, 0x04, 0xf2, 0x7d, 0x5a, 0x03, 0x38,
|
||||
0x1f, 0x69, 0x6d, 0x0b, 0xfb, 0xec, 0x45, 0xfe, 0x8b, 0x7f, 0xec, 0x7b, 0xad, 0xc2, 0x33, 0xf1, 0x9e, 0x9c, 0x71,
|
||||
0xd9, 0xe8, 0x5e, 0x8f, 0xd4, 0xee, 0x87, 0x45, 0x6c, 0xe2, 0x12, 0xf8, 0xb8, 0xc5, 0xee, 0x43, 0xa6, 0x37, 0x91,
|
||||
0xb5, 0x2c, 0x2f, 0xe9, 0xe8, 0x24, 0xd0, 0x45, 0xc1, 0x0c, 0x7c, 0xf0, 0xb2, 0xb5, 0x2d, 0x10, 0x36, 0x7e, 0x18,
|
||||
0x7c, 0x79, 0x82, 0x69, 0x3d, 0x35, 0xca, 0x52, 0xee, 0xc9, 0xb5, 0x65, 0xa4, 0xa1, 0xfd, 0x70, 0x7e, 0xe0, 0x7d,
|
||||
0x67, 0xf9, 0xa1, 0x71, 0xd2, 0x08, 0x74, 0x33, 0x5f, 0x69, 0xa4, 0x59, 0x03, 0xfd, 0xf8, 0xf0, 0x70, 0x1a, 0x50,
|
||||
0x43, 0xfb, 0x61, 0xf0, 0x38, 0x18, 0x88, 0x85, 0x36, 0x23, 0x06, 0x4f, 0x02, 0xbb, 0x78, 0x1a, 0xaa, 0xd2, 0x02,
|
||||
0x5e, 0xa0, 0x74, 0x30, 0xc8, 0x7a, 0x66, 0xab, 0xd9, 0x43, 0x99, 0x45, 0xb7, 0x0c, 0x5c, 0xec, 0xc8, 0x03, 0x0e,
|
||||
0x0b, 0xca, 0x4a, 0x22, 0x48, 0xfb, 0xb7, 0x3d, 0x82, 0x07, 0x8d, 0x1b, 0x21, 0x87, 0x4d, 0x57, 0xa4, 0x5b, 0xd4,
|
||||
0xe3, 0x88, 0x02, 0xc4, 0x81, 0xf9, 0x47, 0xe4, 0xbf, 0x3e, 0x39, 0xbb, 0x4f, 0x7e, 0x91, 0x63, 0x98, 0x97, 0xe4,
|
||||
0x52, 0x01, 0x58, 0xba, 0x32, 0xbf, 0xae, 0xff, 0x45, 0xa1, 0xbc, 0x9b, 0xa4, 0x09, 0x0e, 0x79, 0xc0, 0x41, 0x86,
|
||||
0x52, 0x88, 0x55, 0x39, 0x9d, 0xb6, 0xed, 0x35, 0x68, 0x29, 0xfa, 0xe6, 0x6c, 0x3d, 0x0a, 0xcd, 0x6a, 0x28, 0xfd,
|
||||
0x65, 0x24, 0xce, 0x38, 0x98, 0x01, 0xd9, 0x3f, 0x1b, 0x4c, 0xc4, 0x5c, 0x1d, 0xaa, 0x21, 0x78, 0x67, 0xaf, 0x55,
|
||||
0x72, 0x34, 0xf8, 0x1b, 0x03, 0x21, 0x27, 0x08, 0xbd, 0x59, 0x60, 0x48, 0x0d, 0xe2, 0x56, 0x9b, 0x30, 0x92, 0x8f,
|
||||
0x67, 0x8a, 0x7f, 0x20, 0xbd, 0x2d, 0xfd, 0xc5, 0xb0, 0xa6, 0xaa, 0x77, 0x75, 0x26, 0x33, 0x2f, 0x20, 0x2a, 0xab,
|
||||
0x5c, 0xd1, 0x3b, 0xda, 0xb2, 0x4c, 0xa4, 0x86, 0x25, 0x8d, 0x49, 0x05, 0xaf, 0x7a, 0xa8, 0xd4, 0x9c, 0x0d, 0xd3,
|
||||
0x38, 0xa6, 0x5c, 0x29, 0x6b, 0x16, 0x27, 0x07, 0xf1, 0xbe, 0xe2, 0x24, 0xc1, 0x8d, 0x25, 0x76, 0xbc, 0xf6, 0x0d,
|
||||
0xc2, 0x94, 0x25, 0xb8, 0xf3, 0x07, 0x9a, 0x49, 0xf4, 0x89, 0x82, 0x4d, 0x51, 0xb1, 0x96, 0x61, 0x62, 0x8d, 0xc8,
|
||||
0x61, 0x65, 0x0d, 0x14, 0x34, 0x02, 0x65, 0x94, 0xcc, 0x1d, 0x85, 0x00, 0x0f, 0x1a, 0x57, 0x68, 0x15, 0xcf, 0xa4,
|
||||
0xa2, 0x7d, 0x6d, 0x53, 0x60, 0xce, 0x5c, 0x61, 0x82, 0x17, 0x32, 0xc1, 0x87, 0x02, 0x0c, 0x91, 0x85, 0x57, 0x51,
|
||||
0xbe, 0xb2, 0x38, 0x9f, 0x3d, 0x2a, 0x52, 0x5a, 0xad, 0xba, 0x46, 0x9e, 0x3c, 0x8a, 0xa0, 0x46, 0x15, 0xf4, 0x59,
|
||||
0x74, 0x5f, 0x2a, 0xae, 0x96, 0x56, 0xf0, 0x54, 0x39, 0xaf, 0xac, 0x2a, 0xb9, 0xad, 0x32, 0x50, 0xc9, 0xc1, 0xee,
|
||||
0xd2, 0x0d, 0x34, 0xaa, 0x98, 0x4d, 0x6d, 0x3d, 0xc6, 0xb9, 0x5b, 0x00, 0x5f, 0xea, 0xda, 0x16, 0xa6, 0x08, 0x43,
|
||||
0x58, 0x4d, 0x8d, 0x07, 0x55, 0x62, 0x81, 0x44, 0xcc, 0x31, 0x04, 0x4b, 0x4c, 0x8b, 0x3e, 0xff, 0xd8, 0xf6, 0x65,
|
||||
0x19, 0xa1, 0x94, 0x62, 0x65, 0x0a, 0xdd, 0x60, 0x38, 0xd3, 0xbe, 0x0d, 0xa3, 0x99, 0xd5, 0x37, 0x68, 0xa1, 0x71,
|
||||
0xa3, 0x41, 0xe7, 0xbe, 0x9d, 0x72, 0x84, 0x75, 0xb6, 0x8d, 0x98, 0xd6, 0xb8, 0x2d, 0x43, 0x85, 0x5d, 0xf9, 0xca,
|
||||
0xc3, 0x96, 0xa5, 0xa6, 0xe7, 0x50, 0x88, 0x6b, 0x84, 0x58, 0x44, 0x45, 0x20, 0xdf, 0x1e, 0x5a, 0xc9, 0xce, 0x42,
|
||||
0x2a, 0x1f, 0x3e, 0x3c, 0x7b, 0x68, 0x3c, 0x34, 0x8b, 0x36, 0xba, 0x1f, 0xce, 0x0f, 0xa0, 0x60, 0x37, 0x5f, 0x1a,
|
||||
0x03, 0x2b, 0x86, 0x29, 0x45, 0x7b, 0xb4, 0xb7, 0x06, 0x68, 0x17, 0x7e, 0x13, 0x76, 0x91, 0x4d, 0x27, 0xee, 0xbc,
|
||||
0x7e, 0x80, 0xc2, 0x66, 0xac, 0xc6, 0xbf, 0xeb, 0x7f, 0xd7, 0x84, 0x79, 0xf3, 0xf1, 0xde, 0xec, 0xa6, 0x93, 0xa8,
|
||||
0x13, 0x3b, 0x4a, 0x81, 0xfa, 0x11, 0x1e, 0x4a, 0xd2, 0x50, 0x2a, 0xea, 0x9a, 0xc2, 0x37, 0x08, 0xed, 0x01, 0xf5,
|
||||
0xa2, 0xd5, 0x32, 0x29, 0x49, 0xc4, 0x1a, 0x11, 0xc0, 0xda, 0x24, 0x28, 0x84, 0x38, 0x60, 0x80, 0xcf, 0xd0, 0x45,
|
||||
0x83, 0xa7, 0xca, 0x52, 0x5c, 0xac, 0x23, 0x01};
|
||||
|
||||
// Backwards compatibility alias
|
||||
#define INDEX_GZ INDEX_BR
|
||||
|
||||
#endif // USE_CAPTIVE_PORTAL_GZIP
|
||||
|
||||
} // namespace esphome::captive_portal
|
||||
|
||||
@@ -112,7 +112,11 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
||||
#else
|
||||
auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
|
||||
#endif
|
||||
#ifdef USE_CAPTIVE_PORTAL_GZIP
|
||||
response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
|
||||
#else
|
||||
response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("br"));
|
||||
#endif
|
||||
req->send(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,9 @@ bool CH422GComponent::read_inputs_() {
|
||||
bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
|
||||
auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str());
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "write failed for register 0x%X, error %d", reg, err);
|
||||
this->status_set_warning(buf);
|
||||
return false;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
@@ -104,7 +106,9 @@ uint8_t CH422GComponent::read_reg_(uint8_t reg) {
|
||||
uint8_t value;
|
||||
auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str());
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "read failed for register 0x%X, error %d", reg, err);
|
||||
this->status_set_warning(buf);
|
||||
return 0;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
|
||||
@@ -682,19 +682,19 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) {
|
||||
return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode);
|
||||
}
|
||||
|
||||
bool Climate::set_custom_fan_mode_(const char *mode) {
|
||||
bool Climate::set_custom_fan_mode_(const char *mode, size_t len) {
|
||||
auto traits = this->get_traits();
|
||||
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode),
|
||||
this->has_custom_fan_mode());
|
||||
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode,
|
||||
traits.find_custom_fan_mode_(mode, len), this->has_custom_fan_mode());
|
||||
}
|
||||
|
||||
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
|
||||
|
||||
bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); }
|
||||
|
||||
bool Climate::set_custom_preset_(const char *preset) {
|
||||
bool Climate::set_custom_preset_(const char *preset, size_t len) {
|
||||
auto traits = this->get_traits();
|
||||
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset),
|
||||
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len),
|
||||
this->has_custom_preset());
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "climate_mode.h"
|
||||
#include "climate_traits.h"
|
||||
|
||||
@@ -110,8 +111,8 @@ class ClimateCall {
|
||||
const optional<ClimateFanMode> &get_fan_mode() const;
|
||||
const optional<ClimateSwingMode> &get_swing_mode() const;
|
||||
const optional<ClimatePreset> &get_preset() const;
|
||||
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||
const char *get_custom_preset() const { return this->custom_preset_; }
|
||||
StringRef get_custom_fan_mode() const { return StringRef::from_maybe_nullptr(this->custom_fan_mode_); }
|
||||
StringRef get_custom_preset() const { return StringRef::from_maybe_nullptr(this->custom_preset_); }
|
||||
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
|
||||
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
|
||||
|
||||
@@ -266,11 +267,11 @@ class Climate : public EntityBase {
|
||||
/// The active swing mode of the climate device.
|
||||
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
|
||||
|
||||
/// Get the active custom fan mode (read-only access).
|
||||
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||
/// Get the active custom fan mode (read-only access). Returns StringRef.
|
||||
StringRef get_custom_fan_mode() const { return StringRef::from_maybe_nullptr(this->custom_fan_mode_); }
|
||||
|
||||
/// Get the active custom preset (read-only access).
|
||||
const char *get_custom_preset() const { return this->custom_preset_; }
|
||||
/// Get the active custom preset (read-only access). Returns StringRef.
|
||||
StringRef get_custom_preset() const { return StringRef::from_maybe_nullptr(this->custom_preset_); }
|
||||
|
||||
protected:
|
||||
friend ClimateCall;
|
||||
@@ -280,7 +281,9 @@ class Climate : public EntityBase {
|
||||
bool set_fan_mode_(ClimateFanMode mode);
|
||||
|
||||
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
|
||||
bool set_custom_fan_mode_(const char *mode);
|
||||
bool set_custom_fan_mode_(const char *mode) { return this->set_custom_fan_mode_(mode, strlen(mode)); }
|
||||
bool set_custom_fan_mode_(const char *mode, size_t len);
|
||||
bool set_custom_fan_mode_(StringRef mode) { return this->set_custom_fan_mode_(mode.c_str(), mode.size()); }
|
||||
/// Clear custom fan mode.
|
||||
void clear_custom_fan_mode_();
|
||||
|
||||
@@ -288,7 +291,9 @@ class Climate : public EntityBase {
|
||||
bool set_preset_(ClimatePreset preset);
|
||||
|
||||
/// Set custom preset. Reset primary preset. Return true if preset has been changed.
|
||||
bool set_custom_preset_(const char *preset);
|
||||
bool set_custom_preset_(const char *preset) { return this->set_custom_preset_(preset, strlen(preset)); }
|
||||
bool set_custom_preset_(const char *preset, size_t len);
|
||||
bool set_custom_preset_(StringRef preset) { return this->set_custom_preset_(preset.c_str(), preset.size()); }
|
||||
/// Clear custom preset.
|
||||
void clear_custom_preset_();
|
||||
|
||||
|
||||
@@ -8,20 +8,24 @@ static const char *const TAG = "copy.fan";
|
||||
|
||||
void CopyFan::setup() {
|
||||
source_->add_on_state_callback([this]() {
|
||||
this->state = source_->state;
|
||||
this->oscillating = source_->oscillating;
|
||||
this->speed = source_->speed;
|
||||
this->direction = source_->direction;
|
||||
this->set_preset_mode_(source_->get_preset_mode());
|
||||
this->copy_state_from_source_();
|
||||
this->publish_state();
|
||||
});
|
||||
|
||||
this->copy_state_from_source_();
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void CopyFan::copy_state_from_source_() {
|
||||
this->state = source_->state;
|
||||
this->oscillating = source_->oscillating;
|
||||
this->speed = source_->speed;
|
||||
this->direction = source_->direction;
|
||||
this->set_preset_mode_(source_->get_preset_mode());
|
||||
this->publish_state();
|
||||
if (source_->has_preset_mode()) {
|
||||
this->set_preset_mode_(source_->get_preset_mode());
|
||||
} else {
|
||||
this->clear_preset_mode_();
|
||||
}
|
||||
}
|
||||
|
||||
void CopyFan::dump_config() { LOG_FAN("", "Copy Fan", this); }
|
||||
|
||||
@@ -16,7 +16,7 @@ class CopyFan : public fan::Fan, public Component {
|
||||
|
||||
protected:
|
||||
void control(const fan::FanCall &call) override;
|
||||
;
|
||||
void copy_state_from_source_();
|
||||
|
||||
fan::Fan *source_;
|
||||
};
|
||||
|
||||
@@ -28,24 +28,23 @@ void DebugComponent::dump_config() {
|
||||
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
#endif // USE_SENSOR
|
||||
|
||||
std::string device_info;
|
||||
device_info.reserve(256);
|
||||
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
|
||||
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
|
||||
device_info += ESPHOME_VERSION;
|
||||
size_t pos = buf_append(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
|
||||
|
||||
this->free_heap_ = get_free_heap_();
|
||||
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
|
||||
|
||||
get_device_info_(device_info);
|
||||
pos = get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>(device_info_buffer), pos);
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->device_info_ != nullptr) {
|
||||
if (device_info.length() > 255)
|
||||
device_info.resize(255);
|
||||
this->device_info_->publish_state(device_info);
|
||||
this->device_info_->publish_state(device_info_buffer, pos);
|
||||
}
|
||||
if (this->reset_reason_ != nullptr) {
|
||||
this->reset_reason_->publish_state(get_reset_reason_());
|
||||
char reset_reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
this->reset_reason_->publish_state(
|
||||
get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reset_reason_buffer)));
|
||||
}
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/macros.h"
|
||||
#include <span>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
@@ -15,6 +22,44 @@
|
||||
namespace esphome {
|
||||
namespace debug {
|
||||
|
||||
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
|
||||
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
|
||||
// Format strings must be wrapped with PSTR() macro
|
||||
inline size_t buf_append_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) {
|
||||
if (pos >= size) {
|
||||
return size;
|
||||
}
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int written = vsnprintf_P(buf + pos, size - pos, fmt, args);
|
||||
va_end(args);
|
||||
if (written < 0) {
|
||||
return pos; // encoding error
|
||||
}
|
||||
return std::min(pos + static_cast<size_t>(written), size);
|
||||
}
|
||||
#define buf_append(buf, size, pos, fmt, ...) buf_append_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__)
|
||||
#else
|
||||
/// Safely append formatted string to buffer, returning new position (capped at size)
|
||||
__attribute__((format(printf, 4, 5))) inline size_t buf_append(char *buf, size_t size, size_t pos, const char *fmt,
|
||||
...) {
|
||||
if (pos >= size) {
|
||||
return size;
|
||||
}
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int written = vsnprintf(buf + pos, size - pos, fmt, args);
|
||||
va_end(args);
|
||||
if (written < 0) {
|
||||
return pos; // encoding error
|
||||
}
|
||||
return std::min(pos + static_cast<size_t>(written), size);
|
||||
}
|
||||
#endif
|
||||
|
||||
class DebugComponent : public PollingComponent {
|
||||
public:
|
||||
void loop() override;
|
||||
@@ -81,10 +126,10 @@ class DebugComponent : public PollingComponent {
|
||||
text_sensor::TextSensor *reset_reason_{nullptr};
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
std::string get_reset_reason_();
|
||||
std::string get_wakeup_cause_();
|
||||
const char *get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
|
||||
const char *get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
|
||||
uint32_t get_free_heap_();
|
||||
void get_device_info_(std::string &device_info);
|
||||
size_t get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos);
|
||||
void update_platform_();
|
||||
};
|
||||
|
||||
|
||||
@@ -58,24 +58,29 @@ void DebugComponent::on_shutdown() {
|
||||
global_preferences->sync();
|
||||
}
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() {
|
||||
std::string reset_reason;
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
char *buf = buffer.data();
|
||||
const size_t size = RESET_REASON_BUFFER_SIZE;
|
||||
|
||||
unsigned reason = esp_reset_reason();
|
||||
if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) {
|
||||
reset_reason = RESET_REASONS[reason];
|
||||
if (reason == ESP_RST_SW) {
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
||||
char buffer[REBOOT_MAX_LEN]{};
|
||||
if (pref.load(&buffer)) {
|
||||
buffer[REBOOT_MAX_LEN - 1] = '\0';
|
||||
reset_reason = "Reboot request from " + std::string(buffer);
|
||||
char reboot_source[REBOOT_MAX_LEN]{};
|
||||
if (pref.load(&reboot_source)) {
|
||||
reboot_source[REBOOT_MAX_LEN - 1] = '\0';
|
||||
snprintf(buf, size, "Reboot request from %s", reboot_source);
|
||||
} else {
|
||||
snprintf(buf, size, "%s", RESET_REASONS[reason]);
|
||||
}
|
||||
} else {
|
||||
snprintf(buf, size, "%s", RESET_REASONS[reason]);
|
||||
}
|
||||
} else {
|
||||
reset_reason = "unknown source";
|
||||
snprintf(buf, size, "unknown source");
|
||||
}
|
||||
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
|
||||
return reset_reason;
|
||||
ESP_LOGD(TAG, "Reset Reason: %s", buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static const char *const WAKEUP_CAUSES[] = {
|
||||
@@ -94,7 +99,7 @@ static const char *const WAKEUP_CAUSES[] = {
|
||||
"BT",
|
||||
};
|
||||
|
||||
std::string DebugComponent::get_wakeup_cause_() {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
const char *wake_reason;
|
||||
unsigned reason = esp_sleep_get_wakeup_cause();
|
||||
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
|
||||
@@ -103,6 +108,7 @@ std::string DebugComponent::get_wakeup_cause_() {
|
||||
wake_reason = "unknown source";
|
||||
}
|
||||
ESP_LOGD(TAG, "Wakeup Reason: %s", wake_reason);
|
||||
// Return the static string directly - no need to copy to buffer
|
||||
return wake_reason;
|
||||
}
|
||||
|
||||
@@ -136,7 +142,10 @@ static constexpr ChipFeature CHIP_FEATURES[] = {
|
||||
{CHIP_FEATURE_WIFI_BGN, "2.4GHz WiFi"},
|
||||
};
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
#if defined(USE_ARDUINO)
|
||||
const char *flash_mode;
|
||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
||||
@@ -161,68 +170,66 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
default:
|
||||
flash_mode = "UNKNOWN";
|
||||
}
|
||||
ESP_LOGD(TAG, "Flash Chip: Size=%ukB Speed=%uMHz Mode=%s",
|
||||
ESP.getFlashChipSize() / 1024, // NOLINT
|
||||
ESP.getFlashChipSpeed() / 1000000, flash_mode); // NOLINT
|
||||
device_info += "|Flash: " + to_string(ESP.getFlashChipSize() / 1024) + // NOLINT
|
||||
"kB Speed:" + to_string(ESP.getFlashChipSpeed() / 1000000) + "MHz Mode:"; // NOLINT
|
||||
device_info += flash_mode;
|
||||
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
|
||||
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
|
||||
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
|
||||
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
|
||||
flash_mode);
|
||||
#endif
|
||||
|
||||
esp_chip_info_t info;
|
||||
esp_chip_info(&info);
|
||||
const char *model = ESPHOME_VARIANT;
|
||||
std::string features;
|
||||
|
||||
// Check each known feature bit
|
||||
// Build features string
|
||||
pos = buf_append(buf, size, pos, "|Chip: %s Features:", model);
|
||||
bool first_feature = true;
|
||||
for (const auto &feature : CHIP_FEATURES) {
|
||||
if (info.features & feature.bit) {
|
||||
features += feature.name;
|
||||
features += ", ";
|
||||
pos = buf_append(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
|
||||
first_feature = false;
|
||||
info.features &= ~feature.bit;
|
||||
}
|
||||
}
|
||||
if (info.features != 0)
|
||||
features += "Other:" + format_hex(info.features);
|
||||
ESP_LOGD(TAG, "Chip: Model=%s, Features=%s Cores=%u, Revision=%u", model, features.c_str(), info.cores,
|
||||
info.revision);
|
||||
device_info += "|Chip: ";
|
||||
device_info += model;
|
||||
device_info += " Features:";
|
||||
device_info += features;
|
||||
device_info += " Cores:" + to_string(info.cores);
|
||||
device_info += " Revision:" + to_string(info.revision);
|
||||
device_info += str_sprintf("|CPU Frequency: %" PRIu32 " MHz", arch_get_cpu_freq_hz() / 1000000);
|
||||
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", arch_get_cpu_freq_hz() / 1000000);
|
||||
if (info.features != 0) {
|
||||
pos = buf_append(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
|
||||
}
|
||||
ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision);
|
||||
pos = buf_append(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
|
||||
|
||||
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
|
||||
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
|
||||
pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
|
||||
|
||||
// Framework detection
|
||||
device_info += "|Framework: ";
|
||||
#ifdef USE_ARDUINO
|
||||
ESP_LOGD(TAG, "Framework: Arduino");
|
||||
device_info += "Arduino";
|
||||
pos = buf_append(buf, size, pos, "|Framework: Arduino");
|
||||
#elif defined(USE_ESP32)
|
||||
ESP_LOGD(TAG, "Framework: ESP-IDF");
|
||||
device_info += "ESP-IDF";
|
||||
pos = buf_append(buf, size, pos, "|Framework: ESP-IDF");
|
||||
#else
|
||||
ESP_LOGW(TAG, "Framework: UNKNOWN");
|
||||
device_info += "UNKNOWN";
|
||||
pos = buf_append(buf, size, pos, "|Framework: UNKNOWN");
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
|
||||
device_info += "|ESP-IDF: ";
|
||||
device_info += esp_get_idf_version();
|
||||
pos = buf_append(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
|
||||
|
||||
std::string mac = get_mac_address_pretty();
|
||||
ESP_LOGD(TAG, "EFuse MAC: %s", mac.c_str());
|
||||
device_info += "|EFuse MAC: ";
|
||||
device_info += mac;
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
pos = buf_append(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4],
|
||||
mac[5]);
|
||||
|
||||
device_info += "|Reset: ";
|
||||
device_info += get_reset_reason_();
|
||||
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
|
||||
|
||||
std::string wakeup_reason = this->get_wakeup_cause_();
|
||||
device_info += "|Wakeup: ";
|
||||
device_info += wakeup_reason;
|
||||
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
pos = buf_append(buf, size, pos, "|Wakeup: %s", wakeup_cause);
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
void DebugComponent::update_platform_() {
|
||||
|
||||
@@ -8,19 +8,31 @@ namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() {
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
char *buf = buffer.data();
|
||||
#if !defined(CLANG_TIDY)
|
||||
return ESP.getResetReason().c_str();
|
||||
String reason = ESP.getResetReason(); // NOLINT
|
||||
snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str());
|
||||
return buf;
|
||||
#else
|
||||
return "";
|
||||
buf[0] = '\0';
|
||||
return buf;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
// ESP8266 doesn't have detailed wakeup cause like ESP32
|
||||
return "";
|
||||
}
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() {
|
||||
return ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
const char *flash_mode;
|
||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
||||
case FM_QIO:
|
||||
@@ -38,42 +50,45 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
default:
|
||||
flash_mode = "UNKNOWN";
|
||||
}
|
||||
ESP_LOGD(TAG, "Flash Chip: Size=%ukB Speed=%uMHz Mode=%s",
|
||||
ESP.getFlashChipSize() / 1024, // NOLINT
|
||||
ESP.getFlashChipSpeed() / 1000000, flash_mode); // NOLINT
|
||||
device_info += "|Flash: " + to_string(ESP.getFlashChipSize() / 1024) + // NOLINT
|
||||
"kB Speed:" + to_string(ESP.getFlashChipSpeed() / 1000000) + "MHz Mode:"; // NOLINT
|
||||
device_info += flash_mode;
|
||||
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
|
||||
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
|
||||
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
|
||||
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
|
||||
flash_mode);
|
||||
|
||||
#if !defined(CLANG_TIDY)
|
||||
auto reset_reason = get_reset_reason_();
|
||||
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
uint32_t chip_id = ESP.getChipId();
|
||||
uint8_t boot_version = ESP.getBootVersion();
|
||||
uint8_t boot_mode = ESP.getBootMode();
|
||||
uint8_t cpu_freq = ESP.getCpuFreqMHz();
|
||||
uint32_t flash_chip_id = ESP.getFlashChipId();
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"Chip ID: 0x%08X\n"
|
||||
"Chip ID: 0x%08" PRIX32 "\n"
|
||||
"SDK Version: %s\n"
|
||||
"Core Version: %s\n"
|
||||
"Boot Version=%u Mode=%u\n"
|
||||
"CPU Frequency: %u\n"
|
||||
"Flash Chip ID=0x%08X\n"
|
||||
"Flash Chip ID=0x%08" PRIX32 "\n"
|
||||
"Reset Reason: %s\n"
|
||||
"Reset Info: %s",
|
||||
ESP.getChipId(), ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), ESP.getBootVersion(), ESP.getBootMode(),
|
||||
ESP.getCpuFreqMHz(), ESP.getFlashChipId(), reset_reason.c_str(), ESP.getResetInfo().c_str());
|
||||
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
|
||||
reset_reason, ESP.getResetInfo().c_str());
|
||||
|
||||
device_info += "|Chip: 0x" + format_hex(ESP.getChipId());
|
||||
device_info += "|SDK: ";
|
||||
device_info += ESP.getSdkVersion();
|
||||
device_info += "|Core: ";
|
||||
device_info += ESP.getCoreVersion().c_str();
|
||||
device_info += "|Boot: ";
|
||||
device_info += to_string(ESP.getBootVersion());
|
||||
device_info += "|Mode: " + to_string(ESP.getBootMode());
|
||||
device_info += "|CPU: " + to_string(ESP.getCpuFreqMHz());
|
||||
device_info += "|Flash: 0x" + format_hex(ESP.getFlashChipId());
|
||||
device_info += "|Reset: ";
|
||||
device_info += reset_reason;
|
||||
device_info += "|";
|
||||
device_info += ESP.getResetInfo().c_str();
|
||||
pos = buf_append(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
|
||||
pos = buf_append(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
|
||||
pos = buf_append(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
|
||||
pos = buf_append(buf, size, pos, "|Boot: %u", boot_version);
|
||||
pos = buf_append(buf, size, pos, "|Mode: %u", boot_mode);
|
||||
pos = buf_append(buf, size, pos, "|CPU: %u", cpu_freq);
|
||||
pos = buf_append(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
|
||||
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
|
||||
pos = buf_append(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
|
||||
#endif
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
void DebugComponent::update_platform_() {
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
namespace esphome {
|
||||
namespace debug {
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() { return ""; }
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {}
|
||||
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) { return pos; }
|
||||
|
||||
void DebugComponent::update_platform_() {}
|
||||
|
||||
|
||||
@@ -7,31 +7,43 @@ namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() { return lt_get_reboot_reason_name(lt_get_reboot_reason()); }
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
// Return the static string directly
|
||||
return lt_get_reboot_reason_name(lt_get_reboot_reason());
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); }
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
std::string reset_reason = get_reset_reason_();
|
||||
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
uint32_t flash_kib = lt_flash_get_size() / 1024;
|
||||
uint32_t ram_kib = lt_ram_get_size() / 1024;
|
||||
uint32_t mac_id = lt_cpu_get_mac_id();
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"LibreTiny Version: %s\n"
|
||||
"Chip: %s (%04x) @ %u MHz\n"
|
||||
"Chip ID: 0x%06X\n"
|
||||
"Chip ID: 0x%06" PRIX32 "\n"
|
||||
"Board: %s\n"
|
||||
"Flash: %u KiB / RAM: %u KiB\n"
|
||||
"Flash: %" PRIu32 " KiB / RAM: %" PRIu32 " KiB\n"
|
||||
"Reset Reason: %s",
|
||||
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), lt_cpu_get_mac_id(),
|
||||
lt_get_board_code(), lt_flash_get_size() / 1024, lt_ram_get_size() / 1024, reset_reason.c_str());
|
||||
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
|
||||
lt_get_board_code(), flash_kib, ram_kib, reset_reason);
|
||||
|
||||
device_info += "|Version: ";
|
||||
device_info += LT_BANNER_STR + 10;
|
||||
device_info += "|Reset Reason: ";
|
||||
device_info += reset_reason;
|
||||
device_info += "|Chip Name: ";
|
||||
device_info += lt_cpu_get_model_name();
|
||||
device_info += "|Chip ID: 0x" + format_hex(lt_cpu_get_mac_id());
|
||||
device_info += "|Flash: " + to_string(lt_flash_get_size() / 1024) + " KiB";
|
||||
device_info += "|RAM: " + to_string(lt_ram_get_size() / 1024) + " KiB";
|
||||
pos = buf_append(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
|
||||
pos = buf_append(buf, size, pos, "|Reset Reason: %s", reset_reason);
|
||||
pos = buf_append(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
|
||||
pos = buf_append(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
|
||||
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
|
||||
pos = buf_append(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
void DebugComponent::update_platform_() {
|
||||
|
||||
@@ -7,13 +7,21 @@ namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() { return ""; }
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return rp2040.getFreeHeap(); }
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
ESP_LOGD(TAG, "CPU Frequency: %u", rp2040.f_cpu());
|
||||
device_info += "CPU Frequency: " + to_string(rp2040.f_cpu());
|
||||
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
uint32_t cpu_freq = rp2040.f_cpu();
|
||||
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq);
|
||||
pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
void DebugComponent::update_platform_() {}
|
||||
|
||||
@@ -15,14 +15,14 @@ static const char *const TAG = "debug";
|
||||
constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
|
||||
constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8;
|
||||
|
||||
static void show_reset_reason(std::string &reset_reason, bool set, const char *reason) {
|
||||
static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) {
|
||||
if (!set) {
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
if (!reset_reason.empty()) {
|
||||
reset_reason += ", ";
|
||||
if (pos > 0) {
|
||||
pos = buf_append(buf, size, pos, ", ");
|
||||
}
|
||||
reset_reason += reason;
|
||||
return buf_append(buf, size, pos, "%s", reason);
|
||||
}
|
||||
|
||||
static inline uint32_t read_mem_u32(uintptr_t addr) {
|
||||
@@ -56,33 +56,47 @@ static inline uint32_t sd_version_get() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() {
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
char *buf = buffer.data();
|
||||
const size_t size = RESET_REASON_BUFFER_SIZE;
|
||||
|
||||
uint32_t cause;
|
||||
auto ret = hwinfo_get_reset_cause(&cause);
|
||||
if (ret) {
|
||||
ESP_LOGE(TAG, "Unable to get reset cause: %d", ret);
|
||||
return "";
|
||||
buf[0] = '\0';
|
||||
return buf;
|
||||
}
|
||||
std::string reset_reason;
|
||||
size_t pos = 0;
|
||||
|
||||
show_reset_reason(reset_reason, cause & RESET_PIN, "External pin");
|
||||
show_reset_reason(reset_reason, cause & RESET_SOFTWARE, "Software reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_BROWNOUT, "Brownout (drop in voltage)");
|
||||
show_reset_reason(reset_reason, cause & RESET_POR, "Power-on reset (POR)");
|
||||
show_reset_reason(reset_reason, cause & RESET_WATCHDOG, "Watchdog timer expiration");
|
||||
show_reset_reason(reset_reason, cause & RESET_DEBUG, "Debug event");
|
||||
show_reset_reason(reset_reason, cause & RESET_SECURITY, "Security violation");
|
||||
show_reset_reason(reset_reason, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode");
|
||||
show_reset_reason(reset_reason, cause & RESET_CPU_LOCKUP, "CPU lock-up detected");
|
||||
show_reset_reason(reset_reason, cause & RESET_PARITY, "Parity error");
|
||||
show_reset_reason(reset_reason, cause & RESET_PLL, "PLL error");
|
||||
show_reset_reason(reset_reason, cause & RESET_CLOCK, "Clock error");
|
||||
show_reset_reason(reset_reason, cause & RESET_HARDWARE, "Hardware reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_USER, "User reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_TEMPERATURE, "Temperature reset");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset");
|
||||
pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset");
|
||||
|
||||
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
|
||||
return reset_reason;
|
||||
// Ensure null termination if nothing was written
|
||||
if (pos == 0) {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Reset Reason: %s", buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
// Zephyr doesn't have detailed wakeup cause like ESP32
|
||||
return "";
|
||||
}
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
@@ -118,175 +132,183 @@ void DebugComponent::log_partition_info_() {
|
||||
flash_area_foreach(fa_cb, nullptr);
|
||||
}
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
std::string supply = "Main supply status: ";
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
|
||||
supply += "Normal voltage.";
|
||||
} else {
|
||||
supply += "High voltage.";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", supply.c_str());
|
||||
device_info += "|" + supply;
|
||||
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
std::string reg0 = "Regulator stage 0: ";
|
||||
// Main supply status
|
||||
const char *supply_status =
|
||||
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
|
||||
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
|
||||
pos = buf_append(buf, size, pos, "|Main supply status: %s", supply_status);
|
||||
|
||||
// Regulator stage 0
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
||||
reg0 += nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
reg0 += ", ";
|
||||
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
const char *reg0_voltage;
|
||||
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
|
||||
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "1.8V (default)";
|
||||
reg0_voltage = "1.8V (default)";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "1.8V";
|
||||
reg0_voltage = "1.8V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.1V";
|
||||
reg0_voltage = "2.1V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.4V";
|
||||
reg0_voltage = "2.4V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.7V";
|
||||
reg0_voltage = "2.7V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "3.0V";
|
||||
reg0_voltage = "3.0V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "3.3V";
|
||||
reg0_voltage = "3.3V";
|
||||
break;
|
||||
default:
|
||||
reg0 += "???V";
|
||||
reg0_voltage = "???V";
|
||||
}
|
||||
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
|
||||
pos = buf_append(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
|
||||
} else {
|
||||
reg0 += "disabled";
|
||||
ESP_LOGD(TAG, "Regulator stage 0: disabled");
|
||||
pos = buf_append(buf, size, pos, "|Regulator stage 0: disabled");
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", reg0.c_str());
|
||||
device_info += "|" + reg0;
|
||||
|
||||
std::string reg1 = "Regulator stage 1: ";
|
||||
reg1 += nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
ESP_LOGD(TAG, "%s", reg1.c_str());
|
||||
device_info += "|" + reg1;
|
||||
// Regulator stage 1
|
||||
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
|
||||
pos = buf_append(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
|
||||
|
||||
std::string usb_power = "USB power state: ";
|
||||
// USB power state
|
||||
const char *usb_state;
|
||||
if (nrf_power_usbregstatus_vbusdet_get(NRF_POWER)) {
|
||||
if (nrf_power_usbregstatus_outrdy_get(NRF_POWER)) {
|
||||
/**< From the power viewpoint, USB is ready for working. */
|
||||
usb_power += "ready";
|
||||
usb_state = "ready";
|
||||
} else {
|
||||
/**< The USB power is detected, but USB power regulator is not ready. */
|
||||
usb_power += "connected (regulator is not ready)";
|
||||
usb_state = "connected (regulator is not ready)";
|
||||
}
|
||||
} else {
|
||||
/**< No power on USB lines detected. */
|
||||
usb_power += "disconected";
|
||||
usb_state = "disconnected";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", usb_power.c_str());
|
||||
device_info += "|" + usb_power;
|
||||
ESP_LOGD(TAG, "USB power state: %s", usb_state);
|
||||
pos = buf_append(buf, size, pos, "|USB power state: %s", usb_state);
|
||||
|
||||
// Power-fail comparator
|
||||
bool enabled;
|
||||
nrf_power_pof_thr_t pof_thr;
|
||||
|
||||
pof_thr = nrf_power_pofcon_get(NRF_POWER, &enabled);
|
||||
std::string pof = "Power-fail comparator: ";
|
||||
nrf_power_pof_thr_t pof_thr = nrf_power_pofcon_get(NRF_POWER, &enabled);
|
||||
if (enabled) {
|
||||
const char *pof_voltage;
|
||||
switch (pof_thr) {
|
||||
case POWER_POFCON_THRESHOLD_V17:
|
||||
pof += "1.7V";
|
||||
pof_voltage = "1.7V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V18:
|
||||
pof += "1.8V";
|
||||
pof_voltage = "1.8V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V19:
|
||||
pof += "1.9V";
|
||||
pof_voltage = "1.9V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V20:
|
||||
pof += "2.0V";
|
||||
pof_voltage = "2.0V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V21:
|
||||
pof += "2.1V";
|
||||
pof_voltage = "2.1V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V22:
|
||||
pof += "2.2V";
|
||||
pof_voltage = "2.2V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V23:
|
||||
pof += "2.3V";
|
||||
pof_voltage = "2.3V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V24:
|
||||
pof += "2.4V";
|
||||
pof_voltage = "2.4V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V25:
|
||||
pof += "2.5V";
|
||||
pof_voltage = "2.5V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V26:
|
||||
pof += "2.6V";
|
||||
pof_voltage = "2.6V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V27:
|
||||
pof += "2.7V";
|
||||
pof_voltage = "2.7V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V28:
|
||||
pof += "2.8V";
|
||||
pof_voltage = "2.8V";
|
||||
break;
|
||||
default:
|
||||
pof_voltage = "???V";
|
||||
break;
|
||||
}
|
||||
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
||||
pof += ", VDDH: ";
|
||||
const char *vddh_voltage;
|
||||
switch (nrf_power_pofcon_vddh_get(NRF_POWER)) {
|
||||
case NRF_POWER_POFTHRVDDH_V27:
|
||||
pof += "2.7V";
|
||||
vddh_voltage = "2.7V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V28:
|
||||
pof += "2.8V";
|
||||
vddh_voltage = "2.8V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V29:
|
||||
pof += "2.9V";
|
||||
vddh_voltage = "2.9V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V30:
|
||||
pof += "3.0V";
|
||||
vddh_voltage = "3.0V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V31:
|
||||
pof += "3.1V";
|
||||
vddh_voltage = "3.1V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V32:
|
||||
pof += "3.2V";
|
||||
vddh_voltage = "3.2V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V33:
|
||||
pof += "3.3V";
|
||||
vddh_voltage = "3.3V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V34:
|
||||
pof += "3.4V";
|
||||
vddh_voltage = "3.4V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V35:
|
||||
pof += "3.5V";
|
||||
vddh_voltage = "3.5V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V36:
|
||||
pof += "3.6V";
|
||||
vddh_voltage = "3.6V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V37:
|
||||
pof += "3.7V";
|
||||
vddh_voltage = "3.7V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V38:
|
||||
pof += "3.8V";
|
||||
vddh_voltage = "3.8V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V39:
|
||||
pof += "3.9V";
|
||||
vddh_voltage = "3.9V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V40:
|
||||
pof += "4.0V";
|
||||
vddh_voltage = "4.0V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V41:
|
||||
pof += "4.1V";
|
||||
vddh_voltage = "4.1V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V42:
|
||||
pof += "4.2V";
|
||||
vddh_voltage = "4.2V";
|
||||
break;
|
||||
default:
|
||||
vddh_voltage = "???V";
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
|
||||
pos = buf_append(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
|
||||
pos = buf_append(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
|
||||
}
|
||||
} else {
|
||||
pof += "disabled";
|
||||
ESP_LOGD(TAG, "Power-fail comparator: disabled");
|
||||
pos = buf_append(buf, size, pos, "|Power-fail comparator: disabled");
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", pof.c_str());
|
||||
device_info += "|" + pof;
|
||||
|
||||
auto package = [](uint32_t value) {
|
||||
switch (value) {
|
||||
@@ -300,6 +322,8 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
return "Unspecified";
|
||||
};
|
||||
|
||||
char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
get_mac_address_pretty_into_buffer(mac_pretty);
|
||||
ESP_LOGD(TAG,
|
||||
"Code page size: %u, code size: %u, device id: 0x%08x%08x\n"
|
||||
"Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n"
|
||||
@@ -308,10 +332,10 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
"RAM: %ukB, Flash: %ukB, production test: %sdone",
|
||||
NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0],
|
||||
NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2],
|
||||
NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), get_mac_address_pretty().c_str(),
|
||||
NRF_FICR->INFO.PART, NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF,
|
||||
NRF_FICR->INFO.VARIANT >> 8 & 0xFF, NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE),
|
||||
NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
|
||||
NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART,
|
||||
NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF,
|
||||
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE), NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
|
||||
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
|
||||
bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] &&
|
||||
(NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected
|
||||
<< UICR_PSELRESET_CONNECT_Pos;
|
||||
@@ -373,6 +397,8 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
"NRFFW %s\n"
|
||||
"NRFHW %s",
|
||||
uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str());
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
void DebugComponent::update_platform_() {}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace deep_sleep {
|
||||
// - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup)
|
||||
// - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup)
|
||||
// - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup)
|
||||
// - GPIO wakeup: GPIO wakeup for non-RTC pins (esp_deep_sleep_enable_gpio_wakeup)
|
||||
// - GPIO wakeup: GPIO wakeup for RTC pins (esp_deep_sleep_enable_gpio_wakeup)
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
@@ -127,22 +127,14 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
defined(USE_ESP32_VARIANT_ESP32C61)
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
|
||||
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
|
||||
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
|
||||
} else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) {
|
||||
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
|
||||
}
|
||||
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
||||
gpio_hold_en(gpio_pin);
|
||||
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
|
||||
// Some ESP32 variants support holding a single GPIO during deep sleep without this function
|
||||
// For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
|
||||
gpio_deep_sleep_hold_en();
|
||||
#endif
|
||||
// Make sure GPIO is in input mode, not all RTC GPIO pins are input by default
|
||||
gpio_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
||||
bool level = !this->wakeup_pin_->is_inverted();
|
||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||
level = !level;
|
||||
}
|
||||
// Internal pullup/pulldown resistors are enabled automatically, when
|
||||
// ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS is set (by default it is)
|
||||
esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(),
|
||||
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ std::string MenuItemSelect::get_value_text() const {
|
||||
result = this->value_getter_.value()(this);
|
||||
} else {
|
||||
if (this->select_var_ != nullptr) {
|
||||
result = this->select_var_->current_option();
|
||||
auto option = this->select_var_->current_option();
|
||||
result.assign(option.c_str(), option.size());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -268,7 +268,7 @@ bool Dsmr::parse_telegram() {
|
||||
|
||||
// publish the telegram, after publishing the sensors so it can also trigger action based on latest values
|
||||
if (this->s_telegram_ != nullptr) {
|
||||
this->s_telegram_->publish_state(std::string(this->telegram_, this->bytes_read_));
|
||||
this->s_telegram_->publish_state(this->telegram_, this->bytes_read_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -82,8 +82,9 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||
light_effect->get_last_universe());
|
||||
auto effect_name = light_effect->get_name();
|
||||
ESP_LOGD(TAG, "Registering '%.*s' for universes %d-%d.", (int) effect_name.size(), effect_name.c_str(),
|
||||
light_effect->get_first_universe(), light_effect->get_last_universe());
|
||||
|
||||
light_effects_.push_back(light_effect);
|
||||
|
||||
@@ -98,8 +99,9 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||
light_effect->get_last_universe());
|
||||
auto effect_name = light_effect->get_name();
|
||||
ESP_LOGD(TAG, "Unregistering '%.*s' for universes %d-%d.", (int) effect_name.size(), effect_name.c_str(),
|
||||
light_effect->get_first_universe(), light_effect->get_last_universe());
|
||||
|
||||
// Swap with last element and pop for O(1) removal (order doesn't matter)
|
||||
*it = light_effects_.back();
|
||||
|
||||
@@ -58,8 +58,9 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
|
||||
std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1));
|
||||
auto *input_data = packet.values + 1;
|
||||
|
||||
ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset,
|
||||
output_end);
|
||||
auto effect_name = get_name();
|
||||
ESP_LOGV(TAG, "Applying data for '%.*s' on %d universe, for %" PRId32 "-%d.", (int) effect_name.size(),
|
||||
effect_name.c_str(), universe, output_offset, output_end);
|
||||
|
||||
switch (channels_) {
|
||||
case E131_MONO:
|
||||
|
||||
@@ -116,9 +116,8 @@ void ES8388::setup() {
|
||||
if (this->dac_output_select_ != nullptr) {
|
||||
auto dac_power = this->get_dac_power();
|
||||
if (dac_power.has_value()) {
|
||||
auto dac_power_str = this->dac_output_select_->at(dac_power.value());
|
||||
if (dac_power_str.has_value()) {
|
||||
this->dac_output_select_->publish_state(dac_power_str.value());
|
||||
if (this->dac_output_select_->has_index(dac_power.value())) {
|
||||
this->dac_output_select_->publish_state(dac_power.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
|
||||
}
|
||||
@@ -127,9 +126,8 @@ void ES8388::setup() {
|
||||
if (this->adc_input_mic_select_ != nullptr) {
|
||||
auto mic_input = this->get_mic_input();
|
||||
if (mic_input.has_value()) {
|
||||
auto mic_input_str = this->adc_input_mic_select_->at(mic_input.value());
|
||||
if (mic_input_str.has_value()) {
|
||||
this->adc_input_mic_select_->publish_state(mic_input_str.value());
|
||||
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
|
||||
this->adc_input_mic_select_->publish_state(mic_input.value());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ from esphome.const import (
|
||||
CONF_IGNORE_EFUSE_MAC_CRC,
|
||||
CONF_LOG_LEVEL,
|
||||
CONF_NAME,
|
||||
CONF_OTA,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM_VERSION,
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
@@ -620,11 +621,18 @@ def final_validate(config):
|
||||
)
|
||||
)
|
||||
if advanced[CONF_ENABLE_OTA_ROLLBACK]:
|
||||
safe_mode_config = full_config.get(CONF_SAFE_MODE)
|
||||
if safe_mode_config is None or safe_mode_config.get(CONF_DISABLED, False):
|
||||
_LOGGER.warning(
|
||||
"OTA rollback requires safe_mode, disabling rollback support"
|
||||
)
|
||||
# "disabled: false" means safe mode *is* enabled.
|
||||
safe_mode_config = full_config.get(CONF_SAFE_MODE, {CONF_DISABLED: True})
|
||||
safe_mode_enabled = not safe_mode_config[CONF_DISABLED]
|
||||
ota_enabled = CONF_OTA in full_config
|
||||
# Both need to be enabled for rollback to work
|
||||
if not (ota_enabled and safe_mode_enabled):
|
||||
# But only warn if ota is even possible
|
||||
if ota_enabled:
|
||||
_LOGGER.warning(
|
||||
"OTA rollback requires safe_mode, disabling rollback support"
|
||||
)
|
||||
# disable the rollback feature anyway since it can't be used.
|
||||
advanced[CONF_ENABLE_OTA_ROLLBACK] = False
|
||||
if errs:
|
||||
raise cv.MultipleInvalid(errs)
|
||||
|
||||
@@ -429,9 +429,11 @@ void ESP32Camera::framebuffer_task(void *pv) {
|
||||
camera_fb_t *framebuffer = esp_camera_fb_get();
|
||||
xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY);
|
||||
// Only wake the main loop if there's a pending request to consume the frame
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
if (that->has_requested_image_()) {
|
||||
App.wake_loop_threadsafe();
|
||||
}
|
||||
#endif
|
||||
// return is no-op for config with 1 fb
|
||||
xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY);
|
||||
esp_camera_fb_return(framebuffer);
|
||||
|
||||
@@ -19,6 +19,7 @@ from esphome.components.esp32 import (
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_RX_PIN,
|
||||
CONF_RX_QUEUE_LEN,
|
||||
CONF_TX_PIN,
|
||||
@@ -33,6 +34,13 @@ CONF_TX_ENQUEUE_TIMEOUT = "tx_enqueue_timeout"
|
||||
esp32_can_ns = cg.esphome_ns.namespace("esp32_can")
|
||||
esp32_can = esp32_can_ns.class_("ESP32Can", CanbusComponent)
|
||||
|
||||
# Mode options - consistent with MCP2515 component
|
||||
CanMode = esp32_can_ns.enum("CanMode")
|
||||
CAN_MODES = {
|
||||
"NORMAL": CanMode.CAN_MODE_NORMAL,
|
||||
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
|
||||
}
|
||||
|
||||
# Currently the driver only supports a subset of the bit rates defined in canbus
|
||||
# The supported bit rates differ between ESP32 variants.
|
||||
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
|
||||
@@ -95,6 +103,7 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
|
||||
cv.Optional(CONF_BIT_RATE, default="125KBPS"): validate_bit_rate,
|
||||
cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_number,
|
||||
cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_MODE, default="NORMAL"): cv.enum(CAN_MODES, upper=True),
|
||||
cv.Optional(CONF_RX_QUEUE_LEN): cv.uint32_t,
|
||||
cv.Optional(CONF_TX_QUEUE_LEN): cv.uint32_t,
|
||||
cv.Optional(CONF_TX_ENQUEUE_TIMEOUT): cv.positive_time_period_milliseconds,
|
||||
@@ -117,6 +126,7 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_rx(config[CONF_RX_PIN]))
|
||||
cg.add(var.set_tx(config[CONF_TX_PIN]))
|
||||
cg.add(var.set_mode(config[CONF_MODE]))
|
||||
if (rx_queue_len := config.get(CONF_RX_QUEUE_LEN)) is not None:
|
||||
cg.add(var.set_rx_queue_len(rx_queue_len))
|
||||
if (tx_queue_len := config.get(CONF_TX_QUEUE_LEN)) is not None:
|
||||
|
||||
@@ -75,8 +75,15 @@ bool ESP32Can::setup_internal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select TWAI mode based on configuration
|
||||
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
|
||||
|
||||
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
|
||||
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
|
||||
}
|
||||
|
||||
twai_general_config_t g_config =
|
||||
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL);
|
||||
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, twai_mode);
|
||||
g_config.controller_id = next_twai_ctrl_num++;
|
||||
if (this->tx_queue_len_.has_value()) {
|
||||
g_config.tx_queue_len = this->tx_queue_len_.value();
|
||||
@@ -111,6 +118,12 @@ bool ESP32Can::setup_internal() {
|
||||
}
|
||||
|
||||
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
|
||||
// In listen-only mode, we cannot transmit
|
||||
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
|
||||
ESP_LOGW(TAG, "Cannot send messages in LISTEN_ONLY mode");
|
||||
return canbus::ERROR_FAIL;
|
||||
}
|
||||
|
||||
if (this->twai_handle_ == nullptr) {
|
||||
// not setup yet or setup failed
|
||||
return canbus::ERROR_FAIL;
|
||||
|
||||
@@ -10,10 +10,16 @@
|
||||
namespace esphome {
|
||||
namespace esp32_can {
|
||||
|
||||
enum CanMode : uint8_t {
|
||||
CAN_MODE_NORMAL = 0,
|
||||
CAN_MODE_LISTEN_ONLY = 1,
|
||||
};
|
||||
|
||||
class ESP32Can : public canbus::Canbus {
|
||||
public:
|
||||
void set_rx(int rx) { rx_ = rx; }
|
||||
void set_tx(int tx) { tx_ = tx; }
|
||||
void set_mode(CanMode mode) { mode_ = mode; }
|
||||
void set_tx_queue_len(uint32_t tx_queue_len) { this->tx_queue_len_ = tx_queue_len; }
|
||||
void set_rx_queue_len(uint32_t rx_queue_len) { this->rx_queue_len_ = rx_queue_len; }
|
||||
void set_tx_enqueue_timeout_ms(uint32_t tx_enqueue_timeout_ms) {
|
||||
@@ -28,6 +34,7 @@ class ESP32Can : public canbus::Canbus {
|
||||
|
||||
int rx_{-1};
|
||||
int tx_{-1};
|
||||
CanMode mode_{CAN_MODE_NORMAL};
|
||||
TickType_t tx_enqueue_timeout_ticks_{};
|
||||
optional<uint32_t> tx_queue_len_{};
|
||||
optional<uint32_t> rx_queue_len_{};
|
||||
|
||||
@@ -93,9 +93,9 @@ async def to_code(config):
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
||||
if framework_ver >= cv.Version(5, 5, 0):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.2")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.7.0")
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -4,18 +4,24 @@ from typing import Any
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, update
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_PATH, CONF_RAW_DATA_ID
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.const import CONF_ID, CONF_PATH, CONF_SOURCE, CONF_TYPE
|
||||
from esphome.core import CORE, ID, HexInt
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
AUTO_LOAD = ["sha256", "watchdog"]
|
||||
AUTO_LOAD = ["sha256", "watchdog", "json"]
|
||||
DEPENDENCIES = ["esp32_hosted"]
|
||||
|
||||
CONF_SHA256 = "sha256"
|
||||
CONF_HTTP_REQUEST_ID = "http_request_id"
|
||||
|
||||
TYPE_EMBEDDED = "embedded"
|
||||
TYPE_HTTP = "http"
|
||||
|
||||
esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted")
|
||||
http_request_ns = cg.esphome_ns.namespace("http_request")
|
||||
HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component)
|
||||
Esp32HostedUpdate = esp32_hosted_ns.class_(
|
||||
"Esp32HostedUpdate", update.UpdateEntity, cg.Component
|
||||
"Esp32HostedUpdate", update.UpdateEntity, cg.PollingComponent
|
||||
)
|
||||
|
||||
|
||||
@@ -30,12 +36,29 @@ def _validate_sha256(value: Any) -> str:
|
||||
return value
|
||||
|
||||
|
||||
BASE_SCHEMA = update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
|
||||
cv.polling_component_schema("6h")
|
||||
)
|
||||
|
||||
EMBEDDED_SCHEMA = BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
cv.Required(CONF_SHA256): _validate_sha256,
|
||||
}
|
||||
)
|
||||
|
||||
HTTP_SCHEMA = BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||
cv.Required(CONF_SOURCE): cv.url,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
|
||||
cv.typed_schema(
|
||||
{
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
cv.Required(CONF_SHA256): _validate_sha256,
|
||||
TYPE_EMBEDDED: EMBEDDED_SCHEMA,
|
||||
TYPE_HTTP: HTTP_SCHEMA,
|
||||
}
|
||||
),
|
||||
esp32.only_on_variant(
|
||||
@@ -48,6 +71,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
def _validate_firmware(config: dict[str, Any]) -> None:
|
||||
if config[CONF_TYPE] != TYPE_EMBEDDED:
|
||||
return
|
||||
|
||||
path = CORE.relative_config_path(config[CONF_PATH])
|
||||
with open(path, "rb") as f:
|
||||
firmware_data = f.read()
|
||||
@@ -65,14 +91,22 @@ FINAL_VALIDATE_SCHEMA = _validate_firmware
|
||||
async def to_code(config: dict[str, Any]) -> None:
|
||||
var = await update.new_update(config)
|
||||
|
||||
path = config[CONF_PATH]
|
||||
with open(CORE.relative_config_path(path), "rb") as f:
|
||||
firmware_data = f.read()
|
||||
rhs = [HexInt(x) for x in firmware_data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
if config[CONF_TYPE] == TYPE_EMBEDDED:
|
||||
path = config[CONF_PATH]
|
||||
with open(CORE.relative_config_path(path), "rb") as f:
|
||||
firmware_data = f.read()
|
||||
rhs = [HexInt(x) for x in firmware_data]
|
||||
arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8)
|
||||
prog_arr = cg.progmem_array(arr_id, rhs)
|
||||
|
||||
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
|
||||
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
|
||||
cg.add(var.set_firmware_data(prog_arr))
|
||||
cg.add(var.set_firmware_size(len(firmware_data)))
|
||||
else:
|
||||
http_request_var = await cg.get_variable(config[CONF_HTTP_REQUEST_ID])
|
||||
cg.add(var.set_http_request_parent(http_request_var))
|
||||
cg.add(var.set_source_url(config[CONF_SOURCE]))
|
||||
cg.add_define("USE_ESP32_HOSTED_HTTP_UPDATE")
|
||||
|
||||
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
|
||||
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
|
||||
cg.add(var.set_firmware_data(prog_arr))
|
||||
cg.add(var.set_firmware_size(len(firmware_data)))
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
#include <esp_image_format.h>
|
||||
#include <esp_app_desc.h>
|
||||
#include <esp_hosted.h>
|
||||
#include <esp_hosted_host_fw_ver.h>
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include <esp_hosted_ota.h>
|
||||
@@ -16,18 +22,50 @@ namespace esphome::esp32_hosted {
|
||||
|
||||
static const char *const TAG = "esp32_hosted.update";
|
||||
|
||||
// older coprocessor firmware versions have a 1500-byte limit per RPC call
|
||||
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
|
||||
constexpr size_t CHUNK_SIZE = 1500;
|
||||
|
||||
// Compile-time version string from esp_hosted_host_fw_ver.h macros
|
||||
#define STRINGIFY_(x) #x
|
||||
#define STRINGIFY(x) STRINGIFY_(x)
|
||||
static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_MAJOR_1) "." STRINGIFY(
|
||||
ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1);
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// Parse version string "major.minor.patch" into components
|
||||
// Returns true if parsing succeeded
|
||||
static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) {
|
||||
major = minor = patch = 0;
|
||||
if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare two versions, returns:
|
||||
// -1 if v1 < v2
|
||||
// 0 if v1 == v2
|
||||
// 1 if v1 > v2
|
||||
static int compare_versions(int major1, int minor1, int patch1, int major2, int minor2, int patch2) {
|
||||
if (major1 != major2)
|
||||
return major1 < major2 ? -1 : 1;
|
||||
if (minor1 != minor2)
|
||||
return minor1 < minor2 ? -1 : 1;
|
||||
if (patch1 != patch2)
|
||||
return patch1 < patch2 ? -1 : 1;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Esp32HostedUpdate::setup() {
|
||||
this->update_info_.title = "ESP32 Hosted Coprocessor";
|
||||
|
||||
// if wifi is not present, connect to the coprocessor
|
||||
#ifndef USE_WIFI
|
||||
// If WiFi is not present, connect to the coprocessor
|
||||
esp_hosted_connect_to_slave(); // NOLINT
|
||||
#endif
|
||||
|
||||
// get coprocessor version
|
||||
// Get coprocessor version
|
||||
esp_hosted_coprocessor_fwver_t ver_info;
|
||||
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
|
||||
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
|
||||
@@ -36,7 +74,8 @@ void Esp32HostedUpdate::setup() {
|
||||
}
|
||||
ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
|
||||
|
||||
// get image version
|
||||
#ifndef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// Embedded mode: get image version from embedded firmware
|
||||
const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
|
||||
if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
|
||||
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
|
||||
@@ -64,57 +103,272 @@ void Esp32HostedUpdate::setup() {
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
}
|
||||
|
||||
// publish state
|
||||
// Publish state
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
#else
|
||||
// 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->set_retry("initial_check", 10000, 6, [this](uint8_t) {
|
||||
if (!network::is_connected()) {
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
this->check();
|
||||
return RetryResult::DONE;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Esp32HostedUpdate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ESP32 Hosted Update:\n"
|
||||
" Current Version: %s\n"
|
||||
" Latest Version: %s\n"
|
||||
" Latest Size: %zu bytes",
|
||||
this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(),
|
||||
" Host Library Version: %s\n"
|
||||
" Coprocessor Version: %s\n"
|
||||
" Latest Version: %s",
|
||||
ESP_HOSTED_VERSION_STR, this->update_info_.current_version.c_str(),
|
||||
this->update_info_.latest_version.c_str());
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Mode: HTTP\n"
|
||||
" Source URL: %s",
|
||||
this->source_url_.c_str());
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Mode: Embedded\n"
|
||||
" Firmware Size: %zu bytes",
|
||||
this->firmware_size_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Esp32HostedUpdate::perform(bool force) {
|
||||
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
|
||||
ESP_LOGW(TAG, "Update not available");
|
||||
void Esp32HostedUpdate::check() {
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
if (!network::is_connected()) {
|
||||
ESP_LOGD(TAG, "Network not connected, skipping update check");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->fetch_manifest_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
if (this->update_info_.latest_version.empty() ||
|
||||
this->update_info_.latest_version == this->update_info_.current_version) {
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
} else {
|
||||
this->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
}
|
||||
|
||||
this->update_info_.has_progress = false;
|
||||
this->update_info_.progress = 0.0f;
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
ESP_LOGD(TAG, "Fetching manifest");
|
||||
|
||||
auto container = this->http_request_parent_->get(this->source_url_);
|
||||
if (container == nullptr || container->status_code != 200) {
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
|
||||
this->status_set_error(LOG_STR("Failed to fetch manifest"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read manifest JSON into string (manifest is small, ~1KB max)
|
||||
std::string json_str;
|
||||
json_str.reserve(container->content_length);
|
||||
uint8_t buf[256];
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read = container->read(buf, sizeof(buf));
|
||||
if (read > 0) {
|
||||
json_str.append(reinterpret_cast<char *>(buf), read);
|
||||
}
|
||||
yield();
|
||||
}
|
||||
container->end();
|
||||
|
||||
// Parse JSON manifest
|
||||
// Format: {"versions": [{"version": "2.7.0", "url": "...", "sha256": "..."}]}
|
||||
// Only consider versions <= host library version to avoid compatibility issues
|
||||
bool valid = json::parse_json(json_str, [this](JsonObject root) -> bool {
|
||||
if (!root["versions"].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain 'versions' array");
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonArray versions = root["versions"].as<JsonArray>();
|
||||
if (versions.size() == 0) {
|
||||
ESP_LOGE(TAG, "Manifest 'versions' array is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the highest version that is compatible with the host library
|
||||
// (version <= host version to avoid upgrading coprocessor ahead of host)
|
||||
int best_major = -1, best_minor = -1, best_patch = -1;
|
||||
std::string best_version, best_url, best_sha256;
|
||||
|
||||
for (JsonObject entry : versions) {
|
||||
if (!entry["version"].is<const char *>() || !entry["url"].is<const char *>() ||
|
||||
!entry["sha256"].is<const char *>()) {
|
||||
continue; // Skip malformed entries
|
||||
}
|
||||
|
||||
std::string ver_str = entry["version"].as<std::string>();
|
||||
int major, minor, patch;
|
||||
if (!parse_version(ver_str, major, minor, patch)) {
|
||||
ESP_LOGW(TAG, "Failed to parse version: %s", ver_str.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this version is compatible (not newer than host)
|
||||
if (compare_versions(major, minor, patch, ESP_HOSTED_VERSION_MAJOR_1, ESP_HOSTED_VERSION_MINOR_1,
|
||||
ESP_HOSTED_VERSION_PATCH_1) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is better than our current best
|
||||
if (best_major < 0 || compare_versions(major, minor, patch, best_major, best_minor, best_patch) > 0) {
|
||||
best_major = major;
|
||||
best_minor = minor;
|
||||
best_patch = patch;
|
||||
best_version = ver_str;
|
||||
best_url = entry["url"].as<std::string>();
|
||||
best_sha256 = entry["sha256"].as<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
if (best_major < 0) {
|
||||
ESP_LOGW(TAG, "No compatible firmware version found (host is %s)", ESP_HOSTED_VERSION_STR);
|
||||
return false;
|
||||
}
|
||||
|
||||
this->update_info_.latest_version = best_version;
|
||||
this->firmware_url_ = best_url;
|
||||
|
||||
// Parse SHA256 hex string to bytes
|
||||
if (!parse_hex(best_sha256, this->firmware_sha256_.data(), 32)) {
|
||||
ESP_LOGE(TAG, "Invalid SHA256: %s", best_sha256.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Best compatible version: %s", this->update_info_.latest_version.c_str());
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse manifest JSON");
|
||||
this->status_set_error(LOG_STR("Failed to parse manifest"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
||||
ESP_LOGI(TAG, "Downloading firmware");
|
||||
|
||||
auto container = this->http_request_parent_->get(this->firmware_url_);
|
||||
if (container == nullptr || container->status_code != 200) {
|
||||
ESP_LOGE(TAG, "Failed to fetch firmware");
|
||||
this->status_set_error(LOG_STR("Failed to fetch firmware"));
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t total_size = container->content_length;
|
||||
ESP_LOGI(TAG, "Firmware size: %zu bytes", total_size);
|
||||
|
||||
// Begin OTA on coprocessor
|
||||
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Failed to begin OTA"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stream firmware to coprocessor while computing SHA256
|
||||
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
|
||||
alignas(32) sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
|
||||
uint8_t buffer[CHUNK_SIZE];
|
||||
while (container->get_bytes_read() < total_size) {
|
||||
int read = container->read(buffer, sizeof(buffer));
|
||||
|
||||
// Feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (read <= 0) {
|
||||
if (read < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
}
|
||||
// read == 0: no more data available, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
hasher.add(buffer, read);
|
||||
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Failed to write OTA data"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
container->end();
|
||||
|
||||
// Verify SHA256
|
||||
hasher.calculate();
|
||||
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
|
||||
ESP_LOGE(TAG, "SHA256 mismatch");
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
this->status_set_error(LOG_STR("SHA256 verification failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "SHA256 verified successfully");
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
|
||||
if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
|
||||
ESP_LOGE(TAG, "No firmware data available");
|
||||
return;
|
||||
this->status_set_error(LOG_STR("No firmware data available"));
|
||||
return false;
|
||||
}
|
||||
|
||||
sha256::SHA256 hasher;
|
||||
// Verify SHA256 before writing
|
||||
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
|
||||
alignas(32) sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
hasher.add(this->firmware_data_, this->firmware_size_);
|
||||
hasher.calculate();
|
||||
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
|
||||
ESP_LOGE(TAG, "SHA256 mismatch");
|
||||
this->status_set_error(LOG_STR("SHA256 verification failed"));
|
||||
this->publish_state();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
|
||||
|
||||
watchdog::WatchdogManager watchdog(20000);
|
||||
update::UpdateState prev_state = this->state_;
|
||||
this->state_ = update::UPDATE_STATE_INSTALLING;
|
||||
this->update_info_.has_progress = false;
|
||||
this->publish_state();
|
||||
|
||||
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error(LOG_STR("Failed to begin OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t chunk[CHUNK_SIZE];
|
||||
@@ -127,42 +381,68 @@ void Esp32HostedUpdate::perform(bool force) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error(LOG_STR("Failed to write OTA data"));
|
||||
this->publish_state();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
data_ptr += chunk_size;
|
||||
remaining -= chunk_size;
|
||||
App.feed_wdt();
|
||||
}
|
||||
|
||||
err = esp_hosted_slave_ota_end(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Esp32HostedUpdate::perform(bool force) {
|
||||
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
|
||||
ESP_LOGW(TAG, "Update not available");
|
||||
return;
|
||||
}
|
||||
|
||||
update::UpdateState prev_state = this->state_;
|
||||
this->state_ = update::UPDATE_STATE_INSTALLING;
|
||||
this->update_info_.has_progress = false;
|
||||
this->publish_state();
|
||||
|
||||
watchdog::WatchdogManager watchdog(60000);
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
if (!this->stream_firmware_to_coprocessor_())
|
||||
#else
|
||||
if (!this->write_embedded_firmware_to_coprocessor_())
|
||||
#endif
|
||||
{
|
||||
this->state_ = prev_state;
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
// End OTA and activate new firmware
|
||||
esp_err_t end_err = esp_hosted_slave_ota_end(); // NOLINT
|
||||
if (end_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(end_err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error(LOG_STR("Failed to end OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
// activate new firmware
|
||||
err = esp_hosted_slave_ota_activate(); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
|
||||
esp_err_t activate_err = esp_hosted_slave_ota_activate(); // NOLINT
|
||||
if (activate_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(activate_err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error(LOG_STR("Failed to activate OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
// update state
|
||||
// Update state
|
||||
ESP_LOGI(TAG, "OTA update successful");
|
||||
this->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
|
||||
// schedule a restart to ensure everything is in sync
|
||||
// Schedule a restart to ensure everything is in sync
|
||||
ESP_LOGI(TAG, "Restarting in 1 second");
|
||||
this->set_timeout(1000, []() { App.safe_reboot(); });
|
||||
}
|
||||
|
||||
@@ -5,26 +5,55 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/update/update_entity.h"
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_hosted {
|
||||
|
||||
class Esp32HostedUpdate : public update::UpdateEntity, public Component {
|
||||
class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void update() override { this->check(); } // PollingComponent - delegates to check()
|
||||
void perform(bool force) override;
|
||||
void check() override {}
|
||||
void check() override;
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// HTTP mode setters
|
||||
void set_source_url(const std::string &url) { this->source_url_ = url; }
|
||||
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
|
||||
#else
|
||||
// Embedded mode setters
|
||||
void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; }
|
||||
void set_firmware_size(size_t size) { this->firmware_size_ = size; }
|
||||
void set_firmware_sha256(const std::array<uint8_t, 32> &sha256) { this->firmware_sha256_ = sha256; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// HTTP mode members
|
||||
http_request::HttpRequestComponent *http_request_parent_{nullptr};
|
||||
std::string source_url_;
|
||||
std::string firmware_url_;
|
||||
|
||||
// HTTP mode helpers
|
||||
bool fetch_manifest_();
|
||||
bool stream_firmware_to_coprocessor_();
|
||||
#else
|
||||
// Embedded mode members
|
||||
const uint8_t *firmware_data_{nullptr};
|
||||
size_t firmware_size_{0};
|
||||
std::array<uint8_t, 32> firmware_sha256_;
|
||||
|
||||
// Embedded mode helper
|
||||
bool write_embedded_firmware_to_coprocessor_();
|
||||
#endif
|
||||
|
||||
std::array<uint8_t, 32> firmware_sha256_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::esp32_hosted
|
||||
|
||||
@@ -243,6 +243,16 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
|
||||
uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; }
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
/// Ensure benchmark value is read (v2 touch hardware only).
|
||||
/// Called from multiple places - kept as helper to document shared usage.
|
||||
void ensure_benchmark_read() {
|
||||
if (this->benchmark_ == 0) {
|
||||
touch_pad_read_benchmark(this->touch_pad_, &this->benchmark_);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
friend ESP32TouchComponent;
|
||||
|
||||
|
||||
@@ -102,7 +102,16 @@ void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) {
|
||||
uint32_t value = this->read_touch_value(child->get_touch_pad());
|
||||
// Store the value for get_value() access in lambdas
|
||||
child->value_ = value;
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value);
|
||||
// Read benchmark if not already read
|
||||
child->ensure_benchmark_read();
|
||||
// Calculate difference to help user set threshold
|
||||
// For ESP32-S2/S3 v2: touch detected when value > benchmark + threshold
|
||||
// So threshold should be < (value - benchmark) when touched
|
||||
int32_t difference = static_cast<int32_t>(value) - static_cast<int32_t>(child->benchmark_);
|
||||
ESP_LOGD(TAG,
|
||||
"Touch Pad '%s' (T%d): value=%d, benchmark=%" PRIu32 ", difference=%" PRId32 " (set threshold < %" PRId32
|
||||
" to detect touch)",
|
||||
child->get_name().c_str(), child->get_touch_pad(), value, child->benchmark_, difference, difference);
|
||||
#endif
|
||||
}
|
||||
this->setup_mode_last_log_print_ = now;
|
||||
|
||||
@@ -105,8 +105,10 @@ void ESP32TouchComponent::setup() {
|
||||
touch_pad_set_charge_discharge_times(this->meas_cycle_);
|
||||
touch_pad_set_measurement_interval(this->sleep_cycle_);
|
||||
|
||||
// Configure timeout if needed
|
||||
touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX);
|
||||
// Disable hardware timeout - it causes continuous interrupts with high-capacitance
|
||||
// setups (e.g., pressure sensors under cushions). The periodic release check in
|
||||
// loop() handles state detection reliably without needing hardware timeout.
|
||||
touch_pad_timeout_set(false, TOUCH_PAD_THRESHOLD_MAX);
|
||||
|
||||
// Register ISR handler with interrupt mask
|
||||
esp_err_t err =
|
||||
@@ -314,8 +316,7 @@ void ESP32TouchComponent::loop() {
|
||||
|
||||
size_t pads_off = 0;
|
||||
for (auto *child : this->children_) {
|
||||
if (child->benchmark_ == 0)
|
||||
touch_pad_read_benchmark(child->touch_pad_, &child->benchmark_);
|
||||
child->ensure_benchmark_read();
|
||||
// Handle initial state publication after startup
|
||||
this->publish_initial_state_if_needed_(child, now);
|
||||
|
||||
@@ -354,7 +355,7 @@ void ESP32TouchComponent::loop() {
|
||||
|
||||
void ESP32TouchComponent::on_shutdown() {
|
||||
// Disable interrupts
|
||||
touch_pad_intr_disable(static_cast<touch_pad_intr_mask_t>(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT));
|
||||
touch_pad_intr_disable(TOUCH_PAD_INTR_MASK_ACTIVE);
|
||||
touch_pad_isr_deregister(touch_isr_handler, this);
|
||||
this->cleanup_touch_queue_();
|
||||
|
||||
|
||||
@@ -66,11 +66,17 @@ CONF_WAIT_FOR_SENT = "wait_for_sent"
|
||||
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
|
||||
|
||||
|
||||
def validate_channel(value):
|
||||
if value is None:
|
||||
raise cv.Invalid("channel is required if wifi is not configured")
|
||||
return wifi.validate_channel(value)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESPNowComponent),
|
||||
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): wifi.validate_channel,
|
||||
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): validate_channel,
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address),
|
||||
|
||||
@@ -14,12 +14,15 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS
|
||||
auto ips = ethernet::global_eth_component->get_ip_addresses();
|
||||
if (ips != this->last_ips_) {
|
||||
this->last_ips_ = ips;
|
||||
this->publish_state(ips[0].str());
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ips[0].str_to(buf);
|
||||
this->publish_state(buf);
|
||||
uint8_t sensor = 0;
|
||||
for (auto &ip : ips) {
|
||||
if (ip.is_set()) {
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
this->ip_sensors_[sensor]->publish_state(ip.str());
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
}
|
||||
@@ -64,7 +67,10 @@ class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::Text
|
||||
|
||||
class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor {
|
||||
public:
|
||||
void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); }
|
||||
void setup() override {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::ETHERNET; }
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace event {
|
||||
@@ -44,8 +45,11 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
|
||||
/// Return the event types supported by this event.
|
||||
const FixedVector<const char *> &get_event_types() const { return this->types_; }
|
||||
|
||||
/// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet.
|
||||
const char *get_last_event_type() const { return this->last_event_type_; }
|
||||
/// Return the last triggered event type, or empty StringRef if no event triggered yet.
|
||||
StringRef get_last_event_type() const { return StringRef::from_maybe_nullptr(this->last_event_type_); }
|
||||
|
||||
/// Check if an event has been triggered.
|
||||
bool has_event() const { return this->last_event_type_ != nullptr; }
|
||||
|
||||
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
|
||||
|
||||
|
||||
@@ -212,19 +212,18 @@ class FanPresetSetTrigger : public Trigger<std::string> {
|
||||
public:
|
||||
FanPresetSetTrigger(Fan *state) {
|
||||
state->add_on_state_callback([this, state]() {
|
||||
const auto *preset_mode = state->get_preset_mode();
|
||||
auto preset_mode = state->get_preset_mode();
|
||||
auto should_trigger = preset_mode != this->last_preset_mode_;
|
||||
this->last_preset_mode_ = preset_mode;
|
||||
if (should_trigger) {
|
||||
// Trigger with empty string when nullptr to maintain backward compatibility
|
||||
this->trigger(preset_mode != nullptr ? preset_mode : "");
|
||||
this->trigger(std::string(preset_mode));
|
||||
}
|
||||
});
|
||||
this->last_preset_mode_ = state->get_preset_mode();
|
||||
}
|
||||
|
||||
protected:
|
||||
const char *last_preset_mode_{nullptr};
|
||||
StringRef last_preset_mode_{};
|
||||
};
|
||||
|
||||
} // namespace fan
|
||||
|
||||
@@ -61,7 +61,7 @@ void FanCall::perform() {
|
||||
if (this->direction_.has_value()) {
|
||||
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
|
||||
}
|
||||
if (this->has_preset_mode()) {
|
||||
if (this->preset_mode_ != nullptr) {
|
||||
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
|
||||
}
|
||||
this->parent_.control(*this);
|
||||
@@ -83,7 +83,7 @@ void FanCall::validate_() {
|
||||
*this->binary_state_
|
||||
// ..,and no preset mode will be active...
|
||||
&& !this->has_preset_mode() &&
|
||||
this->parent_.get_preset_mode() == nullptr
|
||||
!this->parent_.has_preset_mode()
|
||||
// ...and neither current nor new speed is available...
|
||||
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
|
||||
// ...set speed to 100%
|
||||
@@ -154,16 +154,16 @@ const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
|
||||
return this->get_traits().find_preset_mode(preset_mode, len);
|
||||
}
|
||||
|
||||
bool Fan::set_preset_mode_(const char *preset_mode) {
|
||||
if (preset_mode == nullptr) {
|
||||
// Treat nullptr as clearing the preset mode
|
||||
bool Fan::set_preset_mode_(const char *preset_mode, size_t len) {
|
||||
if (preset_mode == nullptr || len == 0) {
|
||||
// Treat nullptr or empty string as clearing the preset mode (no valid preset is "")
|
||||
if (this->preset_mode_ == nullptr) {
|
||||
return false; // No change
|
||||
}
|
||||
this->clear_preset_mode_();
|
||||
return true;
|
||||
}
|
||||
const char *validated = this->find_preset_mode_(preset_mode);
|
||||
const char *validated = this->find_preset_mode_(preset_mode, len);
|
||||
if (validated == nullptr || this->preset_mode_ == validated) {
|
||||
return false; // Preset mode not supported or no change
|
||||
}
|
||||
@@ -171,10 +171,31 @@ bool Fan::set_preset_mode_(const char *preset_mode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); }
|
||||
bool Fan::set_preset_mode_(const char *preset_mode) {
|
||||
return this->set_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
|
||||
}
|
||||
|
||||
bool Fan::set_preset_mode_(const std::string &preset_mode) {
|
||||
return this->set_preset_mode_(preset_mode.data(), preset_mode.size());
|
||||
}
|
||||
|
||||
bool Fan::set_preset_mode_(StringRef preset_mode) {
|
||||
// Safe: find_preset_mode_ only uses the input for comparison and returns
|
||||
// a pointer from traits, so the input StringRef's lifetime doesn't matter.
|
||||
return this->set_preset_mode_(preset_mode.c_str(), preset_mode.size());
|
||||
}
|
||||
|
||||
void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
|
||||
|
||||
void Fan::apply_preset_mode_(const FanCall &call) {
|
||||
if (call.has_preset_mode()) {
|
||||
this->set_preset_mode_(call.get_preset_mode());
|
||||
} else if (call.get_speed().has_value()) {
|
||||
// Manually setting speed clears preset (per Home Assistant convention)
|
||||
this->clear_preset_mode_();
|
||||
}
|
||||
}
|
||||
|
||||
void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
||||
void Fan::publish_state() {
|
||||
auto traits = this->get_traits();
|
||||
@@ -192,9 +213,8 @@ void Fan::publish_state() {
|
||||
if (traits.supports_direction()) {
|
||||
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
|
||||
}
|
||||
const char *preset = this->get_preset_mode();
|
||||
if (preset != nullptr) {
|
||||
ESP_LOGD(TAG, " Preset Mode: %s", preset);
|
||||
if (this->preset_mode_ != nullptr) {
|
||||
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
|
||||
}
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
|
||||
@@ -249,12 +269,11 @@ void Fan::save_state_() {
|
||||
state.speed = this->speed;
|
||||
state.direction = this->direction;
|
||||
|
||||
const char *preset = this->get_preset_mode();
|
||||
if (preset != nullptr) {
|
||||
if (this->has_preset_mode()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
// Find index of current preset mode (pointer comparison is safe since preset is from traits)
|
||||
for (size_t i = 0; i < preset_modes.size(); i++) {
|
||||
if (preset_modes[i] == preset) {
|
||||
if (preset_modes[i] == this->preset_mode_) {
|
||||
state.preset_mode = i;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "fan_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -128,8 +129,11 @@ class Fan : public EntityBase {
|
||||
/// Set the restore mode of this fan.
|
||||
void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
|
||||
/// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set)
|
||||
const char *get_preset_mode() const { return this->preset_mode_; }
|
||||
/// Get the current preset mode.
|
||||
/// Returns a StringRef of the string stored in traits, or empty ref if not set.
|
||||
/// The returned ref points to string literals from codegen (static storage).
|
||||
/// Traits are set once at startup and valid for the lifetime of the program.
|
||||
StringRef get_preset_mode() const { return StringRef::from_maybe_nullptr(this->preset_mode_); }
|
||||
|
||||
/// Check if a preset mode is currently active
|
||||
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
|
||||
@@ -146,11 +150,15 @@ class Fan : public EntityBase {
|
||||
void dump_traits_(const char *tag, const char *prefix);
|
||||
|
||||
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
|
||||
/// Passing nullptr or empty string clears the preset mode.
|
||||
bool set_preset_mode_(const char *preset_mode, size_t len);
|
||||
bool set_preset_mode_(const char *preset_mode);
|
||||
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
|
||||
bool set_preset_mode_(const std::string &preset_mode);
|
||||
bool set_preset_mode_(StringRef preset_mode);
|
||||
/// Clear the preset mode
|
||||
void clear_preset_mode_();
|
||||
/// Apply preset mode from a FanCall (handles speed-clears-preset convention)
|
||||
void apply_preset_mode_(const FanCall &call);
|
||||
/// Find and return the matching preset mode pointer from traits, or nullptr if not found.
|
||||
const char *find_preset_mode_(const char *preset_mode);
|
||||
const char *find_preset_mode_(const char *preset_mode, size_t len);
|
||||
|
||||
@@ -232,17 +232,19 @@ void GraphLegend::init(Graph *g) {
|
||||
ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh);
|
||||
|
||||
if (this->values_ != VALUE_POSITION_TYPE_NONE) {
|
||||
std::string valstr =
|
||||
value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
|
||||
char valstr[VALUE_ACCURACY_MAX_LEN];
|
||||
if (this->units_) {
|
||||
valstr += trace->sensor_->get_unit_of_measurement_ref();
|
||||
value_accuracy_with_uom_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals(),
|
||||
trace->sensor_->get_unit_of_measurement_ref());
|
||||
} else {
|
||||
value_accuracy_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
|
||||
}
|
||||
this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh);
|
||||
this->font_value_->measure(valstr, &fw, &fos, &fbl, &fh);
|
||||
if (fw > valw)
|
||||
valw = fw;
|
||||
if (fh > valh)
|
||||
valh = fh;
|
||||
ESP_LOGI(TAGL, " %s %d %d", valstr.c_str(), fw, fh);
|
||||
ESP_LOGI(TAGL, " %s %d %d", valstr, fw, fh);
|
||||
}
|
||||
}
|
||||
// Add extra margin
|
||||
@@ -368,13 +370,15 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of
|
||||
if (legend_->values_ != VALUE_POSITION_TYPE_NONE) {
|
||||
int xv = x + legend_->xv_;
|
||||
int yv = y + legend_->yv_;
|
||||
std::string valstr =
|
||||
value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
|
||||
char valstr[VALUE_ACCURACY_MAX_LEN];
|
||||
if (legend_->units_) {
|
||||
valstr += trace->sensor_->get_unit_of_measurement_ref();
|
||||
value_accuracy_with_uom_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals(),
|
||||
trace->sensor_->get_unit_of_measurement_ref());
|
||||
} else {
|
||||
value_accuracy_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
|
||||
}
|
||||
buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str());
|
||||
ESP_LOGV(TAG, " value: %s", valstr.c_str());
|
||||
buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr);
|
||||
ESP_LOGV(TAG, " value: %s", valstr);
|
||||
}
|
||||
x += legend_->xs_;
|
||||
y += legend_->ys_;
|
||||
|
||||
@@ -57,7 +57,7 @@ void HBridgeFan::control(const fan::FanCall &call) {
|
||||
this->oscillating = *call.get_oscillating();
|
||||
if (call.get_direction().has_value())
|
||||
this->direction = *call.get_direction();
|
||||
this->set_preset_mode_(call.get_preset_mode());
|
||||
this->apply_preset_mode_(call);
|
||||
|
||||
this->write_state_();
|
||||
this->publish_state();
|
||||
|
||||
@@ -181,6 +181,11 @@ void HeatpumpIRClimate::transmit_state() {
|
||||
power_mode_cmd = POWER_ON;
|
||||
operating_mode_cmd = MODE_HEAT;
|
||||
break;
|
||||
// Map HEAT_COOL to hardware AUTO mode (automatic heat/cool changeover based on temperature).
|
||||
// In hardware AUTO mode, the device automatically switches between heating and cooling
|
||||
// based on the current temperature versus the target temperature.
|
||||
// See https://github.com/esphome/esphome/issues/11161 for further discussion.
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
case climate::CLIMATE_MODE_AUTO:
|
||||
power_mode_cmd = POWER_ON;
|
||||
operating_mode_cmd = MODE_AUTO;
|
||||
|
||||
@@ -86,16 +86,19 @@ void HomeassistantNumber::control(float value) {
|
||||
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
|
||||
|
||||
api::HomeassistantActionRequest resp;
|
||||
resp.set_service(SERVICE_NAME);
|
||||
resp.service = SERVICE_NAME;
|
||||
|
||||
resp.data.init(2);
|
||||
auto &entity_id = resp.data.emplace_back();
|
||||
entity_id.set_key(ENTITY_ID_KEY);
|
||||
entity_id.value = this->entity_id_;
|
||||
entity_id.key = ENTITY_ID_KEY;
|
||||
entity_id.value = StringRef(this->entity_id_);
|
||||
|
||||
auto &entity_value = resp.data.emplace_back();
|
||||
entity_value.set_key(VALUE_KEY);
|
||||
entity_value.value = to_string(value);
|
||||
entity_value.key = VALUE_KEY;
|
||||
// Stack buffer - no heap allocation; %g produces shortest representation
|
||||
char value_buf[16];
|
||||
snprintf(value_buf, sizeof(value_buf), "%g", value);
|
||||
entity_value.value = StringRef(value_buf);
|
||||
|
||||
api::global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -47,15 +47,15 @@ void HomeassistantSwitch::write_state(bool state) {
|
||||
|
||||
api::HomeassistantActionRequest resp;
|
||||
if (state) {
|
||||
resp.set_service(SERVICE_ON);
|
||||
resp.service = SERVICE_ON;
|
||||
} else {
|
||||
resp.set_service(SERVICE_OFF);
|
||||
resp.service = SERVICE_OFF;
|
||||
}
|
||||
|
||||
resp.data.init(1);
|
||||
auto &entity_id_kv = resp.data.emplace_back();
|
||||
entity_id_kv.set_key(ENTITY_ID_KEY);
|
||||
entity_id_kv.value = this->entity_id_;
|
||||
entity_id_kv.key = ENTITY_ID_KEY;
|
||||
entity_id_kv.value = StringRef(this->entity_id_);
|
||||
|
||||
api::global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ void HomeassistantTextSensor::setup() {
|
||||
} else {
|
||||
ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_, state.c_str());
|
||||
}
|
||||
this->publish_state(state.str());
|
||||
this->publish_state(state.c_str(), state.size());
|
||||
});
|
||||
}
|
||||
void HomeassistantTextSensor::dump_config() {
|
||||
|
||||
@@ -93,35 +93,36 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||
if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
this_update->update_info_.title = root["name"].as<std::string>();
|
||||
this_update->update_info_.latest_version = root["version"].as<std::string>();
|
||||
this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>();
|
||||
this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>();
|
||||
|
||||
for (auto build : root["builds"].as<JsonArray>()) {
|
||||
if (!build["chipFamily"].is<const char *>()) {
|
||||
for (auto build : root[ESPHOME_F("builds")].as<JsonArray>()) {
|
||||
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
if (build["chipFamily"] == ESPHOME_VARIANT) {
|
||||
if (!build["ota"].is<JsonObject>()) {
|
||||
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
|
||||
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
JsonObject ota = build["ota"].as<JsonObject>();
|
||||
if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
|
||||
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
|
||||
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
|
||||
this_update->update_info_.md5 = ota["md5"].as<std::string>();
|
||||
this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>();
|
||||
this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>();
|
||||
|
||||
if (ota["summary"].is<const char *>())
|
||||
this_update->update_info_.summary = ota["summary"].as<std::string>();
|
||||
if (ota["release_url"].is<const char *>())
|
||||
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
|
||||
if (ota[ESPHOME_F("summary")].is<const char *>())
|
||||
this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>();
|
||||
if (ota[ESPHOME_F("release_url")].is<const char *>())
|
||||
this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -550,12 +550,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
ref="0.2.2",
|
||||
)
|
||||
|
||||
# Set compile-time configuration via defines
|
||||
# Set compile-time configuration via build flags (so external library sees them)
|
||||
if CONF_BIT_DEPTH in config:
|
||||
cg.add_define("HUB75_BIT_DEPTH", config[CONF_BIT_DEPTH])
|
||||
cg.add_build_flag(f"-DHUB75_BIT_DEPTH={config[CONF_BIT_DEPTH]}")
|
||||
|
||||
if CONF_GAMMA_CORRECT in config:
|
||||
cg.add_define("HUB75_GAMMA_MODE", config[CONF_GAMMA_CORRECT])
|
||||
cg.add_build_flag(f"-DHUB75_GAMMA_MODE={config[CONF_GAMMA_CORRECT].enum_value}")
|
||||
|
||||
# Await all pin expressions
|
||||
pin_expressions = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
@@ -37,11 +38,21 @@ image_ns = cg.esphome_ns.namespace("image")
|
||||
|
||||
ImageType = image_ns.enum("ImageType")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ImageMetaData:
|
||||
width: int
|
||||
height: int
|
||||
image_type: str
|
||||
transparency: str
|
||||
|
||||
|
||||
CONF_OPAQUE = "opaque"
|
||||
CONF_CHROMA_KEY = "chroma_key"
|
||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||
CONF_INVERT_ALPHA = "invert_alpha"
|
||||
CONF_IMAGES = "images"
|
||||
KEY_METADATA = "metadata"
|
||||
|
||||
TRANSPARENCY_TYPES = (
|
||||
CONF_OPAQUE,
|
||||
@@ -723,10 +734,38 @@ async def write_image(config, all_frames=False):
|
||||
return prog_arr, width, height, image_type, trans_value, frame_count
|
||||
|
||||
|
||||
async def _image_to_code(entry):
|
||||
"""
|
||||
Convert a single image entry to code and return its metadata.
|
||||
:param entry: The config entry for the image.
|
||||
:return: An ImageMetaData object
|
||||
"""
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
|
||||
cg.new_Pvariable(entry[CONF_ID], prog_arr, width, height, image_type, trans_value)
|
||||
return ImageMetaData(
|
||||
width,
|
||||
height,
|
||||
entry[CONF_TYPE],
|
||||
entry[CONF_TRANSPARENCY],
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# By now the config should be a simple list.
|
||||
for entry in config:
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
|
||||
cg.new_Pvariable(
|
||||
entry[CONF_ID], prog_arr, width, height, image_type, trans_value
|
||||
)
|
||||
cg.add_define("USE_IMAGE")
|
||||
# By now the config will be a simple list.
|
||||
# Use a subkey to allow for other data in the future
|
||||
CORE.data[DOMAIN] = {
|
||||
KEY_METADATA: {
|
||||
entry[CONF_ID].id: await _image_to_code(entry) for entry in config
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_all_image_metadata() -> dict[str, ImageMetaData]:
|
||||
"""Get all image metadata."""
|
||||
return CORE.data.get(DOMAIN, {}).get(KEY_METADATA, {})
|
||||
|
||||
|
||||
def get_image_metadata(image_id: str) -> ImageMetaData | None:
|
||||
"""Get image metadata by ID for use by other components."""
|
||||
return get_all_image_metadata().get(image_id)
|
||||
|
||||
@@ -267,8 +267,10 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
|
||||
continue;
|
||||
// Send each ssid separately to avoid overflowing the buffer
|
||||
std::vector<uint8_t> data = improv::build_rpc_response(
|
||||
improv::GET_WIFI_NETWORKS, {ssid, str_sprintf("%d", scan.get_rssi()), YESNO(scan.get_with_auth())}, false);
|
||||
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);
|
||||
}
|
||||
|
||||
76
esphome/components/infrared/__init__.py
Normal file
76
esphome/components/infrared/__init__.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Infrared component for ESPHome.
|
||||
|
||||
WARNING: This component is EXPERIMENTAL. The API (both Python configuration
|
||||
and C++ interfaces) may change at any time without following the normal
|
||||
breaking changes policy. Use at your own risk.
|
||||
|
||||
Once the API is considered stable, this warning will be removed.
|
||||
"""
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import setup_entity
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
AUTO_LOAD = ["remote_base"]
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
infrared_ns = cg.esphome_ns.namespace("infrared")
|
||||
Infrared = infrared_ns.class_("Infrared", cg.EntityBase, cg.Component)
|
||||
InfraredCall = infrared_ns.class_("InfraredCall")
|
||||
InfraredTraits = infrared_ns.class_("InfraredTraits")
|
||||
|
||||
CONF_INFRARED_ID = "infrared_id"
|
||||
CONF_SUPPORTS_TRANSMITTER = "supports_transmitter"
|
||||
CONF_SUPPORTS_RECEIVER = "supports_receiver"
|
||||
|
||||
|
||||
def infrared_schema(class_: type[cg.MockObjClass]) -> cv.Schema:
|
||||
"""Create a schema for an infrared platform.
|
||||
|
||||
:param class_: The infrared class to use for this schema.
|
||||
:return: An extended schema for infrared configuration.
|
||||
"""
|
||||
entity_schema = cv.ENTITY_BASE_SCHEMA.extend(cv.COMPONENT_SCHEMA)
|
||||
return entity_schema.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def setup_infrared_core_(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
"""Set up core infrared configuration."""
|
||||
await setup_entity(var, config, "infrared")
|
||||
|
||||
|
||||
async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
"""Register an infrared device with the core."""
|
||||
cg.add_define("USE_IR_RF")
|
||||
await cg.register_component(var, config)
|
||||
await setup_infrared_core_(var, config)
|
||||
cg.add(cg.App.register_infrared(var))
|
||||
CORE.register_platform_component("infrared", var)
|
||||
|
||||
|
||||
async def new_infrared(config: ConfigType, *args) -> cg.Pvariable:
|
||||
"""Create a new Infrared instance.
|
||||
|
||||
:param config: Configuration dictionary.
|
||||
:param args: Additional arguments to pass to new_Pvariable.
|
||||
:return: The created Infrared instance.
|
||||
"""
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_infrared(var, config)
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(infrared_ns.using)
|
||||
138
esphome/components/infrared/infrared.cpp
Normal file
138
esphome/components/infrared/infrared.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "infrared.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::infrared {
|
||||
|
||||
static const char *const TAG = "infrared";
|
||||
|
||||
// ========== InfraredCall ==========
|
||||
|
||||
InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
|
||||
this->carrier_frequency_ = frequency;
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
|
||||
this->raw_timings_ = &timings;
|
||||
this->packed_data_ = nullptr; // Clear packed if vector is set
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) {
|
||||
this->packed_data_ = data;
|
||||
this->packed_length_ = length;
|
||||
this->packed_count_ = count;
|
||||
this->raw_timings_ = nullptr; // Clear vector if packed is set
|
||||
return *this;
|
||||
}
|
||||
|
||||
InfraredCall &InfraredCall::set_repeat_count(uint32_t count) {
|
||||
this->repeat_count_ = count;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void InfraredCall::perform() {
|
||||
if (this->parent_ != nullptr) {
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Infrared ==========
|
||||
|
||||
void Infrared::setup() {
|
||||
// Set up traits based on configuration
|
||||
this->traits_.set_supports_transmitter(this->has_transmitter());
|
||||
this->traits_.set_supports_receiver(this->has_receiver());
|
||||
|
||||
// Register as listener for received IR data
|
||||
if (this->receiver_ != nullptr) {
|
||||
this->receiver_->register_listener(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Infrared::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Infrared '%s'\n"
|
||||
" Supports Transmitter: %s\n"
|
||||
" Supports Receiver: %s",
|
||||
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
}
|
||||
|
||||
InfraredCall Infrared::make_call() { return InfraredCall(this); }
|
||||
|
||||
void Infrared::control(const InfraredCall &call) {
|
||||
if (this->transmitter_ == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!call.has_raw_timings()) {
|
||||
ESP_LOGE(TAG, "No raw timings provided");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create transmit data object
|
||||
auto transmit_call = this->transmitter_->transmit();
|
||||
auto *transmit_data = transmit_call.get_data();
|
||||
|
||||
// Set carrier frequency
|
||||
if (call.get_carrier_frequency().has_value()) {
|
||||
transmit_data->set_carrier_frequency(call.get_carrier_frequency().value());
|
||||
}
|
||||
|
||||
// Set timings based on format
|
||||
if (call.is_packed()) {
|
||||
// Zero-copy from packed protobuf data
|
||||
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
|
||||
call.get_packed_count());
|
||||
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(),
|
||||
call.get_repeat_count());
|
||||
} else {
|
||||
// From vector (lambdas/automations)
|
||||
transmit_data->set_data(call.get_raw_timings());
|
||||
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%u", call.get_raw_timings().size(),
|
||||
call.get_repeat_count());
|
||||
}
|
||||
|
||||
// Set repeat count
|
||||
if (call.get_repeat_count() > 0) {
|
||||
transmit_call.set_send_times(call.get_repeat_count());
|
||||
}
|
||||
|
||||
// Perform transmission
|
||||
transmit_call.perform();
|
||||
}
|
||||
|
||||
uint32_t Infrared::get_capability_flags() const {
|
||||
uint32_t flags = 0;
|
||||
|
||||
// Add transmit/receive capability based on traits
|
||||
if (this->traits_.get_supports_transmitter())
|
||||
flags |= InfraredCapability::CAPABILITY_TRANSMITTER;
|
||||
if (this->traits_.get_supports_receiver())
|
||||
flags |= InfraredCapability::CAPABILITY_RECEIVER;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool Infrared::on_receive(remote_base::RemoteReceiveData data) {
|
||||
// Forward received IR data to API server
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
if (api::global_api_server != nullptr) {
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id = this->get_device_id();
|
||||
#else
|
||||
uint32_t device_id = 0;
|
||||
#endif
|
||||
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
|
||||
}
|
||||
#endif
|
||||
return false; // Don't consume the event, allow other listeners to process it
|
||||
}
|
||||
|
||||
} // namespace esphome::infrared
|
||||
130
esphome/components/infrared/infrared.h
Normal file
130
esphome/components/infrared/infrared.h
Normal file
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
|
||||
// WARNING: This component is EXPERIMENTAL. The API may change at any time
|
||||
// without following the normal breaking changes policy. Use at your own risk.
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::infrared {
|
||||
|
||||
/// Capability flags for individual infrared instances
|
||||
enum InfraredCapability : uint32_t {
|
||||
CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals
|
||||
CAPABILITY_RECEIVER = 1 << 1, // Can receive signals
|
||||
};
|
||||
|
||||
/// Forward declarations
|
||||
class Infrared;
|
||||
|
||||
/// InfraredCall - Builder pattern for transmitting infrared signals
|
||||
class InfraredCall {
|
||||
public:
|
||||
explicit InfraredCall(Infrared *parent) : parent_(parent) {}
|
||||
|
||||
/// Set the carrier frequency in Hz
|
||||
InfraredCall &set_carrier_frequency(uint32_t frequency);
|
||||
/// Set the raw timings (positive = mark, negative = space)
|
||||
/// Note: The timings vector must outlive the InfraredCall (zero-copy reference)
|
||||
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
|
||||
/// Set the raw timings from packed protobuf sint32 data (zero-copy from wire)
|
||||
/// Note: The data must outlive the InfraredCall
|
||||
InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
|
||||
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
|
||||
InfraredCall &set_repeat_count(uint32_t count);
|
||||
|
||||
/// Perform the transmission
|
||||
void perform();
|
||||
|
||||
/// Get the carrier frequency
|
||||
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
|
||||
/// Get the raw timings (only valid if set via set_raw_timings, not packed)
|
||||
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
|
||||
/// Check if raw timings have been set (either vector or packed)
|
||||
bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; }
|
||||
/// Check if using packed data format
|
||||
bool is_packed() const { return this->packed_data_ != nullptr; }
|
||||
/// Get packed data (only valid if set via set_raw_timings_packed)
|
||||
const uint8_t *get_packed_data() const { return this->packed_data_; }
|
||||
uint16_t get_packed_length() const { return this->packed_length_; }
|
||||
uint16_t get_packed_count() const { return this->packed_count_; }
|
||||
/// Get the repeat count
|
||||
uint32_t get_repeat_count() const { return this->repeat_count_; }
|
||||
|
||||
protected:
|
||||
uint32_t repeat_count_{1};
|
||||
Infrared *parent_;
|
||||
optional<uint32_t> carrier_frequency_;
|
||||
// Vector-based timings (for lambdas/automations)
|
||||
const std::vector<int32_t> *raw_timings_{nullptr};
|
||||
// Packed protobuf timings (for API zero-copy)
|
||||
const uint8_t *packed_data_{nullptr};
|
||||
uint16_t packed_length_{0};
|
||||
uint16_t packed_count_{0};
|
||||
};
|
||||
|
||||
/// InfraredTraits - Describes the capabilities of an infrared implementation
|
||||
class InfraredTraits {
|
||||
public:
|
||||
bool get_supports_transmitter() const { return this->supports_transmitter_; }
|
||||
void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; }
|
||||
|
||||
bool get_supports_receiver() const { return this->supports_receiver_; }
|
||||
void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; }
|
||||
|
||||
protected:
|
||||
bool supports_transmitter_{false};
|
||||
bool supports_receiver_{false};
|
||||
};
|
||||
|
||||
/// Infrared - Base class for infrared remote control implementations
|
||||
class Infrared : public Component, public EntityBase, public remote_base::RemoteReceiverListener {
|
||||
public:
|
||||
Infrared() = default;
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
/// Set the remote receiver component
|
||||
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
|
||||
/// Set the remote transmitter component
|
||||
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
|
||||
|
||||
/// Check if this infrared has a transmitter configured
|
||||
bool has_transmitter() const { return this->transmitter_ != nullptr; }
|
||||
/// Check if this infrared has a receiver configured
|
||||
bool has_receiver() const { return this->receiver_ != nullptr; }
|
||||
|
||||
/// Get the traits for this infrared implementation
|
||||
InfraredTraits &get_traits() { return this->traits_; }
|
||||
const InfraredTraits &get_traits() const { return this->traits_; }
|
||||
|
||||
/// Create a call object for transmitting
|
||||
InfraredCall make_call();
|
||||
|
||||
/// Get capability flags for this infrared instance
|
||||
uint32_t get_capability_flags() const;
|
||||
|
||||
/// Called when IR data is received (from RemoteReceiverListener)
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
|
||||
protected:
|
||||
friend class InfraredCall;
|
||||
|
||||
/// Perform the actual transmission (called by InfraredCall)
|
||||
virtual void control(const InfraredCall &call);
|
||||
|
||||
// Underlying hardware components
|
||||
remote_base::RemoteReceiverBase *receiver_{nullptr};
|
||||
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
|
||||
|
||||
// Traits describing capabilities
|
||||
InfraredTraits traits_;
|
||||
};
|
||||
|
||||
} // namespace esphome::infrared
|
||||
@@ -183,11 +183,14 @@ uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_va
|
||||
return_code = this->read_register(register_to_read, &read_buffer[3], 3);
|
||||
if (return_code != i2c::NO_ERROR) {
|
||||
// Error on the i2c bus
|
||||
this->status_set_warning(
|
||||
str_sprintf("Error code %d when reading from register 0x%02X", return_code, register_to_read).c_str());
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "Error code %d when reading from register 0x%02X", return_code, register_to_read);
|
||||
this->status_set_warning(buf);
|
||||
} else if (crc8(read_buffer, 5, 0x00, 0x07, true) != read_buffer[5]) {
|
||||
// I2C indicated OK, but the CRC of the data does not matcth.
|
||||
this->status_set_warning(str_sprintf("CRC error reading from register 0x%02X", register_to_read).c_str());
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "CRC error reading from register 0x%02X", register_to_read);
|
||||
this->status_set_warning(buf);
|
||||
} else {
|
||||
*register_value = ((uint16_t) read_buffer[4] << 8) | (uint16_t) read_buffer[3];
|
||||
return i2c::NO_ERROR;
|
||||
@@ -225,8 +228,9 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set)
|
||||
if (return_code == i2c::NO_ERROR) {
|
||||
return return_code;
|
||||
} else {
|
||||
this->status_set_warning(
|
||||
str_sprintf("Error code %d when writing to register 0x%02X", return_code, register_to_set).c_str());
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "Error code %d when writing to register 0x%02X", return_code, register_to_set);
|
||||
this->status_set_warning(buf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,8 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
|
||||
{OUT_PIN_LEVEL_HIGH, "high"},
|
||||
};
|
||||
|
||||
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
|
||||
|
||||
// Helper functions for lookups
|
||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
|
||||
for (const auto &entry : arr) {
|
||||
@@ -258,9 +260,10 @@ void LD2410Component::read_all_info() {
|
||||
this->query_parameters_();
|
||||
this->set_config_mode_(false);
|
||||
#ifdef USE_SELECT
|
||||
const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
|
||||
if (this->baud_rate_select_ != nullptr) {
|
||||
this->baud_rate_select_->publish_state(baud_rate);
|
||||
if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) {
|
||||
this->baud_rate_select_->publish_state(*index);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -439,7 +442,8 @@ bool LD2410Component::handle_ack_data_() {
|
||||
ESP_LOGV(TAG, "Baud rate change");
|
||||
#ifdef USE_SELECT
|
||||
if (this->baud_rate_select_ != nullptr) {
|
||||
ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
|
||||
auto baud = this->baud_rate_select_->current_option();
|
||||
ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -763,10 +767,10 @@ void LD2410Component::set_light_out_control() {
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
|
||||
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option());
|
||||
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
|
||||
}
|
||||
if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
|
||||
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option());
|
||||
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
|
||||
}
|
||||
#endif
|
||||
this->set_config_mode_(true);
|
||||
|
||||
@@ -128,6 +128,8 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
|
||||
{OUT_PIN_LEVEL_HIGH, "high"},
|
||||
};
|
||||
|
||||
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
|
||||
|
||||
// Helper functions for lookups
|
||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
|
||||
for (const auto &entry : arr) {
|
||||
@@ -293,9 +295,10 @@ void LD2412Component::read_all_info() {
|
||||
#endif
|
||||
this->set_config_mode_(false);
|
||||
#ifdef USE_SELECT
|
||||
const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
|
||||
if (this->baud_rate_select_ != nullptr) {
|
||||
this->baud_rate_select_->publish_state(baud_rate);
|
||||
if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) {
|
||||
this->baud_rate_select_->publish_state(*index);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -483,7 +486,8 @@ bool LD2412Component::handle_ack_data_() {
|
||||
ESP_LOGV(TAG, "Baud rate change");
|
||||
#ifdef USE_SELECT
|
||||
if (this->baud_rate_select_ != nullptr) {
|
||||
ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
|
||||
auto baud = this->baud_rate_select_->current_option();
|
||||
ESP_LOGW(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -787,7 +791,7 @@ void LD2412Component::set_basic_config() {
|
||||
1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()),
|
||||
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()),
|
||||
#else
|
||||
0x01, // Default value if not using select
|
||||
#endif
|
||||
@@ -841,7 +845,7 @@ void LD2412Component::set_light_out_control() {
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
|
||||
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option());
|
||||
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
|
||||
}
|
||||
#endif
|
||||
uint8_t value[2] = {this->light_function_, this->light_threshold_};
|
||||
|
||||
@@ -88,6 +88,9 @@ constexpr StringToUint8 ZONE_TYPE_BY_STR[] = {
|
||||
{"Filter", ZONE_FILTER},
|
||||
};
|
||||
|
||||
// Baud rates in the same order as BAUD_RATES_BY_STR for index-based lookup
|
||||
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
|
||||
|
||||
// Helper functions for lookups
|
||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
|
||||
for (const auto &entry : arr) {
|
||||
@@ -376,9 +379,10 @@ void LD2450Component::read_all_info() {
|
||||
this->query_zone_();
|
||||
this->set_config_mode_(false);
|
||||
#ifdef USE_SELECT
|
||||
const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
|
||||
if (this->baud_rate_select_ != nullptr && strcmp(this->baud_rate_select_->current_option(), baud_rate.c_str()) != 0) {
|
||||
this->baud_rate_select_->publish_state(baud_rate);
|
||||
if (this->baud_rate_select_ != nullptr) {
|
||||
if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) {
|
||||
this->baud_rate_select_->publish_state(*index);
|
||||
}
|
||||
}
|
||||
this->publish_zone_type();
|
||||
#endif
|
||||
@@ -633,7 +637,8 @@ bool LD2450Component::handle_ack_data_() {
|
||||
ESP_LOGV(TAG, "Baud rate change");
|
||||
#ifdef USE_SELECT
|
||||
if (this->baud_rate_select_ != nullptr) {
|
||||
ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
|
||||
auto baud = this->baud_rate_select_->current_option();
|
||||
ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -710,11 +715,12 @@ bool LD2450Component::handle_ack_data_() {
|
||||
|
||||
case CMD_QUERY_ZONE:
|
||||
ESP_LOGV(TAG, "Query zone conf");
|
||||
this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16);
|
||||
this->zone_type_ = this->buffer_data_[10];
|
||||
this->publish_zone_type();
|
||||
#ifdef USE_SELECT
|
||||
if (this->zone_type_select_ != nullptr) {
|
||||
ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->current_option());
|
||||
auto zone = this->zone_type_select_->current_option();
|
||||
ESP_LOGV(TAG, "Change zone type to: %.*s", (int) zone.size(), zone.c_str());
|
||||
}
|
||||
#endif
|
||||
if (this->buffer_data_[10] == 0x00) {
|
||||
@@ -812,9 +818,8 @@ void LD2450Component::set_zone_type(const char *state) {
|
||||
// Publish Zone Type to Select component
|
||||
void LD2450Component::publish_zone_type() {
|
||||
#ifdef USE_SELECT
|
||||
std::string zone_type = find_str(ZONE_TYPE_BY_UINT, this->zone_type_);
|
||||
if (this->zone_type_select_ != nullptr) {
|
||||
this->zone_type_select_->publish_state(zone_type);
|
||||
this->zone_type_select_->publish_state(find_str(ZONE_TYPE_BY_UINT, this->zone_type_));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
|
||||
namespace esphome::ld24xx {
|
||||
|
||||
// Helper to find index of value in constexpr array
|
||||
template<size_t N> optional<size_t> find_index(const uint32_t (&arr)[N], uint32_t value) {
|
||||
for (size_t i = 0; i < N; i++) {
|
||||
if (arr[i] == value)
|
||||
return i;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static const char *const UNKNOWN_MAC = "unknown";
|
||||
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user