mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 16:51:52 +00:00
Compare commits
16 Commits
no_new_to_
...
pylontech_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38aeb9be37 | ||
|
|
86f91eed2f | ||
|
|
41cecbfb0f | ||
|
|
9315da79bc | ||
|
|
155447f541 | ||
|
|
238e40966f | ||
|
|
f9192b5f75 | ||
|
|
2917057da8 | ||
|
|
c7c9ffe7e1 | ||
|
|
368ef5687b | ||
|
|
b7dc975331 | ||
|
|
44f308502e | ||
|
|
ec477801ca | ||
|
|
c3622ef7fb | ||
|
|
e4ad2082bc | ||
|
|
7afd0eb1aa |
@@ -1 +1 @@
|
||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
||||
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
|
||||
|
||||
@@ -12,7 +12,6 @@ from .const import (
|
||||
CORE_SUBCATEGORY_PATTERNS,
|
||||
DEMANGLED_PATTERNS,
|
||||
ESPHOME_COMPONENT_PATTERN,
|
||||
SECTION_TO_ATTR,
|
||||
SYMBOL_PATTERNS,
|
||||
)
|
||||
from .demangle import batch_demangle
|
||||
@@ -91,6 +90,17 @@ class ComponentMemory:
|
||||
bss_size: int = 0 # Uninitialized data (ram only)
|
||||
symbol_count: int = 0
|
||||
|
||||
def add_section_size(self, section_name: str, size: int) -> None:
|
||||
"""Add size to the appropriate attribute for a section."""
|
||||
if section_name == ".text":
|
||||
self.text_size += size
|
||||
elif section_name == ".rodata":
|
||||
self.rodata_size += size
|
||||
elif section_name == ".data":
|
||||
self.data_size += size
|
||||
elif section_name == ".bss":
|
||||
self.bss_size += size
|
||||
|
||||
@property
|
||||
def flash_total(self) -> int:
|
||||
"""Total flash usage (text + rodata + data)."""
|
||||
@@ -167,12 +177,15 @@ class MemoryAnalyzer:
|
||||
self._elf_symbol_names: set[str] = set()
|
||||
# SDK symbols not in ELF (static/local symbols from closed-source libs)
|
||||
self._sdk_symbols: list[SDKSymbol] = []
|
||||
# CSWTCH symbols: list of (name, size, source_file, component)
|
||||
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
|
||||
|
||||
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_cswtch_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
return dict(self.components)
|
||||
|
||||
@@ -255,8 +268,7 @@ class MemoryAnalyzer:
|
||||
comp_mem.symbol_count += 1
|
||||
|
||||
# Update the appropriate size attribute based on section
|
||||
if attr_name := SECTION_TO_ATTR.get(section_name):
|
||||
setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size)
|
||||
comp_mem.add_section_size(section_name, size)
|
||||
|
||||
# Track uncategorized symbols
|
||||
if component == "other" and size > 0:
|
||||
@@ -372,6 +384,205 @@ class MemoryAnalyzer:
|
||||
|
||||
return "Other Core"
|
||||
|
||||
def _find_object_files_dir(self) -> Path | None:
|
||||
"""Find the directory containing object files for this build.
|
||||
|
||||
Returns:
|
||||
Path to the directory containing .o files, or None if not found.
|
||||
"""
|
||||
# The ELF is typically at .pioenvs/<env>/firmware.elf
|
||||
# Object files are in .pioenvs/<env>/src/ and .pioenvs/<env>/lib*/
|
||||
pioenvs_dir = self.elf_path.parent
|
||||
if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")):
|
||||
return pioenvs_dir
|
||||
return None
|
||||
|
||||
def _scan_cswtch_in_objects(
|
||||
self, obj_dir: Path
|
||||
) -> dict[str, list[tuple[str, int]]]:
|
||||
"""Scan object files for CSWTCH symbols using a single nm invocation.
|
||||
|
||||
Uses ``nm --print-file-name -S`` on all ``.o`` files at once.
|
||||
Output format: ``/path/to/file.o:address size type name``
|
||||
|
||||
Args:
|
||||
obj_dir: Directory containing object files (.pioenvs/<env>/)
|
||||
|
||||
Returns:
|
||||
Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples.
|
||||
"""
|
||||
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
|
||||
if not self.nm_path:
|
||||
return cswtch_map
|
||||
|
||||
# Find all .o files recursively, sorted for deterministic output
|
||||
obj_files = sorted(obj_dir.rglob("*.o"))
|
||||
if not obj_files:
|
||||
return cswtch_map
|
||||
|
||||
_LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files))
|
||||
|
||||
# Single nm call with --print-file-name for all object files
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files],
|
||||
timeout=30,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
return cswtch_map
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if "CSWTCH$" not in line:
|
||||
continue
|
||||
|
||||
# Split on last ":" that precedes a hex address.
|
||||
# nm --print-file-name format: filepath:hex_addr hex_size type name
|
||||
# We split from the right: find the last colon followed by hex digits.
|
||||
parts_after_colon = line.rsplit(":", 1)
|
||||
if len(parts_after_colon) != 2:
|
||||
continue
|
||||
|
||||
file_path = parts_after_colon[0]
|
||||
fields = parts_after_colon[1].split()
|
||||
# fields: [address, size, type, name]
|
||||
if len(fields) < 4:
|
||||
continue
|
||||
|
||||
sym_name = fields[3]
|
||||
if not sym_name.startswith("CSWTCH$"):
|
||||
continue
|
||||
|
||||
try:
|
||||
size = int(fields[1], 16)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Get relative path from obj_dir for readability
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(obj_dir))
|
||||
except ValueError:
|
||||
rel_path = file_path
|
||||
|
||||
key = f"{sym_name}:{size}"
|
||||
cswtch_map[key].append((rel_path, size))
|
||||
|
||||
return cswtch_map
|
||||
|
||||
def _source_file_to_component(self, source_file: str) -> str:
|
||||
"""Map a source object file path to its component name.
|
||||
|
||||
Args:
|
||||
source_file: Relative path like 'src/esphome/components/wifi/wifi_component.cpp.o'
|
||||
|
||||
Returns:
|
||||
Component name like '[esphome]wifi' or the source file if unknown.
|
||||
"""
|
||||
parts = Path(source_file).parts
|
||||
|
||||
# ESPHome component: src/esphome/components/<name>/...
|
||||
if "components" in parts:
|
||||
idx = parts.index("components")
|
||||
if idx + 1 < len(parts):
|
||||
component_name = parts[idx + 1]
|
||||
if component_name in get_esphome_components():
|
||||
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
|
||||
if component_name in self.external_components:
|
||||
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
|
||||
|
||||
# ESPHome core: src/esphome/core/... or src/esphome/...
|
||||
if "core" in parts and "esphome" in parts:
|
||||
return _COMPONENT_CORE
|
||||
if "esphome" in parts and "components" not in parts:
|
||||
return _COMPONENT_CORE
|
||||
|
||||
# Framework/library files - return the first path component
|
||||
# e.g., lib65b/ESPAsyncTCP/... -> lib65b
|
||||
# FrameworkArduino/... -> FrameworkArduino
|
||||
return parts[0] if parts else source_file
|
||||
|
||||
def _analyze_cswtch_symbols(self) -> None:
|
||||
"""Analyze CSWTCH (GCC switch table) symbols by tracing to source objects.
|
||||
|
||||
CSWTCH symbols are compiler-generated lookup tables for switch statements.
|
||||
They are local symbols, so the same name can appear in different object files.
|
||||
This method scans .o files to attribute them to their source components.
|
||||
"""
|
||||
obj_dir = self._find_object_files_dir()
|
||||
if obj_dir is None:
|
||||
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
|
||||
return
|
||||
|
||||
# Scan object files for CSWTCH symbols
|
||||
cswtch_map = self._scan_cswtch_in_objects(obj_dir)
|
||||
if not cswtch_map:
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files")
|
||||
return
|
||||
|
||||
# Collect CSWTCH symbols from the ELF (already parsed in sections)
|
||||
# Include section_name for re-attribution of component totals
|
||||
elf_cswtch = [
|
||||
(symbol_name, size, section_name)
|
||||
for section_name, section in self.sections.items()
|
||||
for symbol_name, size, _ in section.symbols
|
||||
if symbol_name.startswith("CSWTCH$")
|
||||
]
|
||||
|
||||
_LOGGER.debug(
|
||||
"Found %d CSWTCH symbols in ELF, %d unique in object files",
|
||||
len(elf_cswtch),
|
||||
len(cswtch_map),
|
||||
)
|
||||
|
||||
# Match ELF CSWTCH symbols to source files and re-attribute component totals.
|
||||
# _categorize_symbols() already ran and put these into "other" since CSWTCH$
|
||||
# names don't match any component pattern. We move the bytes to the correct
|
||||
# component based on the object file mapping.
|
||||
other_mem = self.components.get("other")
|
||||
|
||||
for sym_name, size, section_name in elf_cswtch:
|
||||
key = f"{sym_name}:{size}"
|
||||
sources = cswtch_map.get(key, [])
|
||||
|
||||
if len(sources) == 1:
|
||||
source_file = sources[0][0]
|
||||
component = self._source_file_to_component(source_file)
|
||||
elif len(sources) > 1:
|
||||
# Ambiguous - multiple object files have same CSWTCH name+size
|
||||
source_file = "ambiguous"
|
||||
component = "ambiguous"
|
||||
_LOGGER.debug(
|
||||
"Ambiguous CSWTCH %s (%d B) found in %d files: %s",
|
||||
sym_name,
|
||||
size,
|
||||
len(sources),
|
||||
", ".join(src for src, _ in sources),
|
||||
)
|
||||
else:
|
||||
source_file = "unknown"
|
||||
component = "unknown"
|
||||
|
||||
self._cswtch_symbols.append((sym_name, size, source_file, component))
|
||||
|
||||
# Re-attribute from "other" to the correct component
|
||||
if (
|
||||
component not in ("other", "unknown", "ambiguous")
|
||||
and other_mem is not None
|
||||
):
|
||||
other_mem.add_section_size(section_name, -size)
|
||||
if component not in self.components:
|
||||
self.components[component] = ComponentMemory(component)
|
||||
self.components[component].add_section_size(section_name, size)
|
||||
|
||||
# Sort by size descending
|
||||
self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
|
||||
_LOGGER.debug(
|
||||
"CSWTCH analysis: %d symbols, %d bytes total",
|
||||
len(self._cswtch_symbols),
|
||||
total_size,
|
||||
)
|
||||
|
||||
def get_unattributed_ram(self) -> tuple[int, int, int]:
|
||||
"""Get unattributed RAM sizes (SDK/framework overhead).
|
||||
|
||||
|
||||
@@ -184,6 +184,52 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
|
||||
)
|
||||
|
||||
def _add_cswtch_analysis(self, lines: list[str]) -> None:
|
||||
"""Add CSWTCH (GCC switch table lookup) analysis section."""
|
||||
self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)")
|
||||
|
||||
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
|
||||
lines.append(
|
||||
f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
# Group by component
|
||||
by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
|
||||
for sym_name, size, source_file, component in self._cswtch_symbols:
|
||||
by_component[component].append((sym_name, size, source_file))
|
||||
|
||||
# Sort components by total size descending
|
||||
sorted_components = sorted(
|
||||
by_component.items(),
|
||||
key=lambda x: sum(s[1] for s in x[1]),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for component, symbols in sorted_components:
|
||||
comp_total = sum(s[1] for s in symbols)
|
||||
lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):")
|
||||
|
||||
# Group by source file within component
|
||||
by_file: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
for sym_name, size, source_file in symbols:
|
||||
by_file[source_file].append((sym_name, size))
|
||||
|
||||
for source_file, file_symbols in sorted(
|
||||
by_file.items(),
|
||||
key=lambda x: sum(s[1] for s in x[1]),
|
||||
reverse=True,
|
||||
):
|
||||
file_total = sum(s[1] for s in file_symbols)
|
||||
lines.append(
|
||||
f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)"
|
||||
)
|
||||
for sym_name, size in sorted(
|
||||
file_symbols, key=lambda x: x[1], reverse=True
|
||||
):
|
||||
lines.append(f" {size:>6,} B {sym_name}")
|
||||
lines.append("")
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -471,6 +517,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f" ... and {len(large_ram_syms) - 10} more")
|
||||
lines.append("")
|
||||
|
||||
# CSWTCH (GCC switch table) analysis
|
||||
if self._cswtch_symbols:
|
||||
self._add_cswtch_analysis(lines)
|
||||
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
)
|
||||
|
||||
@@ -66,15 +66,6 @@ SECTION_MAPPING = {
|
||||
),
|
||||
}
|
||||
|
||||
# Section to ComponentMemory attribute mapping
|
||||
# Maps section names to the attribute name in ComponentMemory dataclass
|
||||
SECTION_TO_ATTR = {
|
||||
".text": "text_size",
|
||||
".rodata": "rodata_size",
|
||||
".data": "data_size",
|
||||
".bss": "bss_size",
|
||||
}
|
||||
|
||||
# Component identification rules
|
||||
# Symbol patterns: patterns found in raw symbol names
|
||||
SYMBOL_PATTERNS = {
|
||||
@@ -513,7 +504,9 @@ SYMBOL_PATTERNS = {
|
||||
"__FUNCTION__$",
|
||||
"DAYS_IN_MONTH",
|
||||
"_DAYS_BEFORE_MONTH",
|
||||
"CSWTCH$",
|
||||
# Note: CSWTCH$ symbols are GCC switch table lookup tables.
|
||||
# They are attributed to their source object files via _analyze_cswtch_symbols()
|
||||
# rather than being lumped into libc.
|
||||
"dst$",
|
||||
"sulp",
|
||||
"_strtol_l", # String to long with locale
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
#include "alarm_control_panel_state.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::alarm_control_panel {
|
||||
|
||||
// Alarm control panel state strings indexed by AlarmControlPanelState enum (0-9)
|
||||
PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT",
|
||||
"ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN");
|
||||
|
||||
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
|
||||
switch (state) {
|
||||
case ACP_STATE_DISARMED:
|
||||
return LOG_STR("DISARMED");
|
||||
case ACP_STATE_ARMED_HOME:
|
||||
return LOG_STR("ARMED_HOME");
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
return LOG_STR("ARMED_AWAY");
|
||||
case ACP_STATE_ARMED_NIGHT:
|
||||
return LOG_STR("ARMED_NIGHT");
|
||||
case ACP_STATE_ARMED_VACATION:
|
||||
return LOG_STR("ARMED_VACATION");
|
||||
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||
return LOG_STR("ARMED_CUSTOM_BYPASS");
|
||||
case ACP_STATE_PENDING:
|
||||
return LOG_STR("PENDING");
|
||||
case ACP_STATE_ARMING:
|
||||
return LOG_STR("ARMING");
|
||||
case ACP_STATE_DISARMING:
|
||||
return LOG_STR("DISARMING");
|
||||
case ACP_STATE_TRIGGERED:
|
||||
return LOG_STR("TRIGGERED");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return AlarmControlPanelStateStrings::get_log_str(static_cast<uint8_t>(state),
|
||||
AlarmControlPanelStateStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
} // namespace esphome::alarm_control_panel
|
||||
|
||||
@@ -1,109 +1,44 @@
|
||||
#include "climate_mode.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::climate {
|
||||
|
||||
// Climate mode strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO
|
||||
PROGMEM_STRING_TABLE(ClimateModeStrings, "OFF", "HEAT_COOL", "COOL", "HEAT", "FAN_ONLY", "DRY", "AUTO", "UNKNOWN");
|
||||
|
||||
const LogString *climate_mode_to_string(ClimateMode mode) {
|
||||
switch (mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case CLIMATE_MODE_HEAT_COOL:
|
||||
return LOG_STR("HEAT_COOL");
|
||||
case CLIMATE_MODE_AUTO:
|
||||
return LOG_STR("AUTO");
|
||||
case CLIMATE_MODE_COOL:
|
||||
return LOG_STR("COOL");
|
||||
case CLIMATE_MODE_HEAT:
|
||||
return LOG_STR("HEAT");
|
||||
case CLIMATE_MODE_FAN_ONLY:
|
||||
return LOG_STR("FAN_ONLY");
|
||||
case CLIMATE_MODE_DRY:
|
||||
return LOG_STR("DRY");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return ClimateModeStrings::get_log_str(static_cast<uint8_t>(mode), ClimateModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN
|
||||
PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN");
|
||||
|
||||
const LogString *climate_action_to_string(ClimateAction action) {
|
||||
switch (action) {
|
||||
case CLIMATE_ACTION_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case CLIMATE_ACTION_COOLING:
|
||||
return LOG_STR("COOLING");
|
||||
case CLIMATE_ACTION_HEATING:
|
||||
return LOG_STR("HEATING");
|
||||
case CLIMATE_ACTION_IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case CLIMATE_ACTION_DRYING:
|
||||
return LOG_STR("DRYING");
|
||||
case CLIMATE_ACTION_FAN:
|
||||
return LOG_STR("FAN");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return ClimateActionStrings::get_log_str(static_cast<uint8_t>(action), ClimateActionStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate fan mode strings indexed by ClimateFanMode enum (0-9): ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS,
|
||||
// DIFFUSE, QUIET
|
||||
PROGMEM_STRING_TABLE(ClimateFanModeStrings, "ON", "OFF", "AUTO", "LOW", "MEDIUM", "HIGH", "MIDDLE", "FOCUS", "DIFFUSE",
|
||||
"QUIET", "UNKNOWN");
|
||||
|
||||
const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) {
|
||||
switch (fan_mode) {
|
||||
case climate::CLIMATE_FAN_ON:
|
||||
return LOG_STR("ON");
|
||||
case climate::CLIMATE_FAN_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
return LOG_STR("AUTO");
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
return LOG_STR("LOW");
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
return LOG_STR("MEDIUM");
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
return LOG_STR("HIGH");
|
||||
case climate::CLIMATE_FAN_MIDDLE:
|
||||
return LOG_STR("MIDDLE");
|
||||
case climate::CLIMATE_FAN_FOCUS:
|
||||
return LOG_STR("FOCUS");
|
||||
case climate::CLIMATE_FAN_DIFFUSE:
|
||||
return LOG_STR("DIFFUSE");
|
||||
case climate::CLIMATE_FAN_QUIET:
|
||||
return LOG_STR("QUIET");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return ClimateFanModeStrings::get_log_str(static_cast<uint8_t>(fan_mode), ClimateFanModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate swing mode strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL
|
||||
PROGMEM_STRING_TABLE(ClimateSwingModeStrings, "OFF", "BOTH", "VERTICAL", "HORIZONTAL", "UNKNOWN");
|
||||
|
||||
const LogString *climate_swing_mode_to_string(ClimateSwingMode swing_mode) {
|
||||
switch (swing_mode) {
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
return LOG_STR("BOTH");
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
return LOG_STR("VERTICAL");
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
return LOG_STR("HORIZONTAL");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return ClimateSwingModeStrings::get_log_str(static_cast<uint8_t>(swing_mode), ClimateSwingModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate preset strings indexed by ClimatePreset enum (0-7): NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY
|
||||
PROGMEM_STRING_TABLE(ClimatePresetStrings, "NONE", "HOME", "AWAY", "BOOST", "COMFORT", "ECO", "SLEEP", "ACTIVITY",
|
||||
"UNKNOWN");
|
||||
|
||||
const LogString *climate_preset_to_string(ClimatePreset preset) {
|
||||
switch (preset) {
|
||||
case climate::CLIMATE_PRESET_NONE:
|
||||
return LOG_STR("NONE");
|
||||
case climate::CLIMATE_PRESET_HOME:
|
||||
return LOG_STR("HOME");
|
||||
case climate::CLIMATE_PRESET_ECO:
|
||||
return LOG_STR("ECO");
|
||||
case climate::CLIMATE_PRESET_AWAY:
|
||||
return LOG_STR("AWAY");
|
||||
case climate::CLIMATE_PRESET_BOOST:
|
||||
return LOG_STR("BOOST");
|
||||
case climate::CLIMATE_PRESET_COMFORT:
|
||||
return LOG_STR("COMFORT");
|
||||
case climate::CLIMATE_PRESET_SLEEP:
|
||||
return LOG_STR("SLEEP");
|
||||
case climate::CLIMATE_PRESET_ACTIVITY:
|
||||
return LOG_STR("ACTIVITY");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return ClimatePresetStrings::get_log_str(static_cast<uint8_t>(preset), ClimatePresetStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
} // namespace esphome::climate
|
||||
|
||||
@@ -19,17 +19,11 @@ const LogString *cover_command_to_str(float pos) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN
|
||||
PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN");
|
||||
|
||||
const LogString *cover_operation_to_str(CoverOperation op) {
|
||||
switch (op) {
|
||||
case COVER_OPERATION_IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case COVER_OPERATION_OPENING:
|
||||
return LOG_STR("OPENING");
|
||||
case COVER_OPERATION_CLOSING:
|
||||
return LOG_STR("CLOSING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return CoverOperationStrings::get_log_str(static_cast<uint8_t>(op), CoverOperationStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
Cover::Cover() : position{COVER_OPEN} {}
|
||||
|
||||
@@ -66,7 +66,7 @@ async def to_code(config):
|
||||
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
|
||||
|
||||
# DSMR Parser
|
||||
cg.add_library("esphome/dsmr_parser", "1.0.0")
|
||||
cg.add_library("esphome/dsmr_parser", "1.1.0")
|
||||
|
||||
# Crypto
|
||||
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0")
|
||||
|
||||
@@ -718,14 +718,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("fw_core_version"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("fw_module_version"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_core_version"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_module_version"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("telegram"): text_sensor.text_sensor_schema().extend(
|
||||
{cv.Optional(CONF_INTERNAL, default=True): cv.boolean}
|
||||
),
|
||||
|
||||
@@ -10,20 +10,11 @@
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#endif
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
using raw_adv_data_t = struct {
|
||||
uint8_t *data;
|
||||
size_t length;
|
||||
esp_power_level_t power_level;
|
||||
};
|
||||
|
||||
class ESPBTUUID;
|
||||
|
||||
class BLEAdvertising {
|
||||
|
||||
@@ -53,8 +53,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range(
|
||||
min=-128, max=0
|
||||
),
|
||||
cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All(
|
||||
cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True)
|
||||
cv.OnlyWithout(CONF_TX_POWER, "esp32_hosted", default="3dBm"): cv.All(
|
||||
cv.conflicts_with_component("esp32_hosted"),
|
||||
cv.decibel,
|
||||
cv.enum(esp32_ble.TX_POWER_LEVELS, int=True),
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
@@ -82,7 +84,10 @@ async def to_code(config):
|
||||
cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL]))
|
||||
cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL]))
|
||||
cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
|
||||
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
|
||||
|
||||
# TX power control only available on native Bluetooth (not ESP-Hosted)
|
||||
if CONF_TX_POWER in config:
|
||||
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
|
||||
|
||||
cg.add_define("USE_ESP32_BLE_ADVERTISING")
|
||||
|
||||
|
||||
@@ -36,11 +36,16 @@ void ESP32BLEBeacon::dump_config() {
|
||||
}
|
||||
}
|
||||
*bpos = '\0';
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d"
|
||||
", TX Power: %ddBm",
|
||||
uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_,
|
||||
(this->tx_power_ * 3) - 12);
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d",
|
||||
uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_);
|
||||
#endif
|
||||
}
|
||||
|
||||
float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||
@@ -74,11 +79,14 @@ void ESP32BLEBeacon::on_advertise_() {
|
||||
ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_);
|
||||
ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(this->measured_power_);
|
||||
|
||||
esp_err_t err;
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
ESP_LOGD(TAG, "Setting BLE TX power");
|
||||
esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_);
|
||||
err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
#endif
|
||||
err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err));
|
||||
|
||||
@@ -48,7 +48,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented
|
||||
void set_min_interval(uint16_t val) { this->min_interval_ = val; }
|
||||
void set_max_interval(uint16_t val) { this->max_interval_ = val; }
|
||||
void set_measured_power(int8_t val) { this->measured_power_ = val; }
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; }
|
||||
#endif
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
|
||||
protected:
|
||||
@@ -60,7 +62,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented
|
||||
uint16_t min_interval_{};
|
||||
uint16_t max_interval_{};
|
||||
int8_t measured_power_{};
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
esp_power_level_t tx_power_{};
|
||||
#endif
|
||||
esp_ble_adv_params_t ble_adv_params_;
|
||||
bool advertising_{false};
|
||||
};
|
||||
|
||||
@@ -2,21 +2,18 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
static const char *const TAG = "fan";
|
||||
|
||||
// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN
|
||||
PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN");
|
||||
|
||||
const LogString *fan_direction_to_string(FanDirection direction) {
|
||||
switch (direction) {
|
||||
case FanDirection::FORWARD:
|
||||
return LOG_STR("FORWARD");
|
||||
case FanDirection::REVERSE:
|
||||
return LOG_STR("REVERSE");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return FanDirectionStrings::get_log_str(static_cast<uint8_t>(direction), FanDirectionStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "gpio_binary_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace gpio {
|
||||
@@ -7,17 +8,12 @@ namespace gpio {
|
||||
static const char *const TAG = "gpio.binary_sensor";
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
// Interrupt type strings indexed by edge-triggered InterruptType values:
|
||||
// indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0).
|
||||
PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE");
|
||||
|
||||
static const LogString *interrupt_type_to_string(gpio::InterruptType type) {
|
||||
switch (type) {
|
||||
case gpio::INTERRUPT_RISING_EDGE:
|
||||
return LOG_STR("RISING_EDGE");
|
||||
case gpio::INTERRUPT_FALLING_EDGE:
|
||||
return LOG_STR("FALLING_EDGE");
|
||||
case gpio::INTERRUPT_ANY_EDGE:
|
||||
return LOG_STR("ANY_EDGE");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return InterruptTypeStrings::get_log_str(static_cast<uint8_t>(type), 0);
|
||||
}
|
||||
|
||||
static const LogString *gpio_mode_to_string(bool use_interrupt) {
|
||||
|
||||
@@ -133,20 +133,10 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
|
||||
// HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length).
|
||||
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
|
||||
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
|
||||
// early return check (bytes_read_ >= content_length) will never trigger.
|
||||
//
|
||||
// TODO: Chunked transfer encoding is NOT properly supported on Arduino.
|
||||
// The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where
|
||||
// esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr()
|
||||
// returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead
|
||||
// of decoded content. This wasn't noticed because requests would complete and payloads
|
||||
// were only examined on IDF. The long transfer times were also masked by the misleading
|
||||
// "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues:
|
||||
// 1. Response body is corrupted - contains chunk size headers mixed with data
|
||||
// 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout
|
||||
// The proper fix would be to use getString() for chunked responses, which decodes chunks
|
||||
// internally, but this buffers the entire response in memory.
|
||||
// The read() method uses a chunked transfer encoding decoder (read_chunked_) to strip
|
||||
// chunk framing and deliver only decoded content. When the final 0-size chunk is received,
|
||||
// is_chunked_ is cleared and content_length is set to the actual decoded size, so
|
||||
// is_read_complete() returns true and callers exit their read loops correctly.
|
||||
int content_length = container->client_.getSize();
|
||||
ESP_LOGD(TAG, "Content-Length: %d", content_length);
|
||||
container->content_length = (size_t) content_length;
|
||||
@@ -174,6 +164,10 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
// > 0: bytes read
|
||||
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
|
||||
// < 0: error/connection closed <-- connection closed returns -1, not 0
|
||||
//
|
||||
// For chunked transfer encoding, read_chunked_() decodes chunk framing and delivers
|
||||
// only the payload data. When the final 0-size chunk is received, it clears is_chunked_
|
||||
// and sets content_length = bytes_read_ so is_read_complete() returns true.
|
||||
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
@@ -184,24 +178,42 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
if (this->is_chunked_) {
|
||||
int result = this->read_chunked_(buf, max_len, stream_ptr);
|
||||
this->duration_ms += (millis() - start);
|
||||
if (result > 0) {
|
||||
return result;
|
||||
}
|
||||
// result <= 0: check for completion or errors
|
||||
if (this->is_read_complete()) {
|
||||
return 0; // Chunked transfer complete (final 0-size chunk received)
|
||||
}
|
||||
if (result < 0) {
|
||||
return result; // Stream error during chunk decoding
|
||||
}
|
||||
// read_chunked_ returned 0: no data was available (available() was 0).
|
||||
// This happens when the TCP buffer is empty - either more data is in flight,
|
||||
// or the connection dropped. Arduino's connected() returns false only when
|
||||
// both the remote has closed AND the receive buffer is empty, so any buffered
|
||||
// data is fully drained before we report the drop.
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
}
|
||||
|
||||
// Non-chunked path
|
||||
int available_data = stream_ptr->available();
|
||||
// For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when
|
||||
// cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read.
|
||||
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
|
||||
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
// Check if we've read all expected content (non-chunked only)
|
||||
// For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
|
||||
if (this->is_read_complete()) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
// For chunked encoding, !connected() after reading means EOF (all chunks received)
|
||||
// For known content_length with bytes_read_ < content_length, it means connection dropped
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
}
|
||||
@@ -215,6 +227,143 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
return read_len;
|
||||
}
|
||||
|
||||
void HttpContainerArduino::chunk_header_complete_() {
|
||||
if (this->chunk_remaining_ == 0) {
|
||||
this->chunk_state_ = ChunkedState::CHUNK_TRAILER;
|
||||
this->chunk_remaining_ = 1; // repurpose as at-start-of-line flag
|
||||
} else {
|
||||
this->chunk_state_ = ChunkedState::CHUNK_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
// Chunked transfer encoding decoder
|
||||
//
|
||||
// On Arduino, getStreamPtr() returns raw TCP data. For chunked responses, this includes
|
||||
// chunk framing (size headers, CRLF delimiters) mixed with payload data. This decoder
|
||||
// strips the framing and delivers only decoded content to the caller.
|
||||
//
|
||||
// Chunk format (RFC 9112 Section 7.1):
|
||||
// <hex-size>[;extension]\r\n
|
||||
// <data bytes>\r\n
|
||||
// ...
|
||||
// 0\r\n
|
||||
// [trailer-field\r\n]*
|
||||
// \r\n
|
||||
//
|
||||
// Non-blocking: only processes bytes already in the TCP receive buffer.
|
||||
// State (chunk_state_, chunk_remaining_) is preserved between calls, so partial
|
||||
// chunk headers or split \r\n sequences resume correctly on the next call.
|
||||
// Framing bytes (hex sizes, \r\n) may be consumed without producing output;
|
||||
// the caller sees 0 and retries via the normal read timeout logic.
|
||||
//
|
||||
// WiFiClient::read() returns -1 on error despite available() > 0 (connection reset
|
||||
// between check and read). On any stream error (c < 0 or readBytes <= 0), we return
|
||||
// already-decoded data if any; otherwise HTTP_ERROR_CONNECTION_CLOSED. The error
|
||||
// will surface again on the next call since the stream stays broken.
|
||||
//
|
||||
// Returns: > 0 decoded bytes, 0 no data available, < 0 error
|
||||
int HttpContainerArduino::read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream) {
|
||||
int total_decoded = 0;
|
||||
|
||||
while (total_decoded < (int) max_len && this->chunk_state_ != ChunkedState::COMPLETE) {
|
||||
// Non-blocking: only process what's already buffered
|
||||
if (stream->available() == 0)
|
||||
break;
|
||||
|
||||
// CHUNK_DATA reads multiple bytes; handle before the single-byte switch
|
||||
if (this->chunk_state_ == ChunkedState::CHUNK_DATA) {
|
||||
// Only read what's available, what fits in buf, and what remains in this chunk
|
||||
size_t to_read =
|
||||
std::min({max_len - (size_t) total_decoded, this->chunk_remaining_, (size_t) stream->available()});
|
||||
if (to_read == 0)
|
||||
break;
|
||||
App.feed_wdt();
|
||||
int read_len = stream->readBytes(buf + total_decoded, to_read);
|
||||
if (read_len <= 0)
|
||||
return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED;
|
||||
total_decoded += read_len;
|
||||
this->chunk_remaining_ -= read_len;
|
||||
this->bytes_read_ += read_len;
|
||||
if (this->chunk_remaining_ == 0)
|
||||
this->chunk_state_ = ChunkedState::CHUNK_DATA_TRAIL;
|
||||
continue;
|
||||
}
|
||||
|
||||
// All other states consume a single byte
|
||||
int c = stream->read();
|
||||
if (c < 0)
|
||||
return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED;
|
||||
|
||||
switch (this->chunk_state_) {
|
||||
// Parse hex chunk size, one byte at a time: "<hex>[;ext]\r\n"
|
||||
// Note: if no hex digits are parsed (e.g., bare \r\n), chunk_remaining_ stays 0
|
||||
// and is treated as the final chunk. This is intentionally lenient — on embedded
|
||||
// devices, rejecting malformed framing is less useful than terminating cleanly.
|
||||
// Overflow of chunk_remaining_ from extremely long hex strings (>8 digits on
|
||||
// 32-bit) is not checked; >4GB chunks are unrealistic on embedded targets and
|
||||
// would simply cause fewer bytes to be read from that chunk.
|
||||
case ChunkedState::CHUNK_HEADER:
|
||||
if (c == '\n') {
|
||||
// \n terminates the size line; chunk_remaining_ == 0 means last chunk
|
||||
this->chunk_header_complete_();
|
||||
} else {
|
||||
uint8_t hex = parse_hex_char(c);
|
||||
if (hex != INVALID_HEX_CHAR) {
|
||||
this->chunk_remaining_ = (this->chunk_remaining_ << 4) | hex;
|
||||
} else if (c != '\r') {
|
||||
this->chunk_state_ = ChunkedState::CHUNK_HEADER_EXT; // ';' starts extension, skip to \n
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Skip chunk extension bytes until \n (e.g., ";name=value\r\n")
|
||||
case ChunkedState::CHUNK_HEADER_EXT:
|
||||
if (c == '\n') {
|
||||
this->chunk_header_complete_();
|
||||
}
|
||||
break;
|
||||
|
||||
// Consume \r\n trailing each chunk's data
|
||||
case ChunkedState::CHUNK_DATA_TRAIL:
|
||||
if (c == '\n') {
|
||||
this->chunk_state_ = ChunkedState::CHUNK_HEADER;
|
||||
this->chunk_remaining_ = 0; // reset for next chunk's hex accumulation
|
||||
}
|
||||
// else: \r is consumed silently, next iteration gets \n
|
||||
break;
|
||||
|
||||
// Consume optional trailer headers and terminating empty line after final chunk.
|
||||
// Per RFC 9112 Section 7.1: "0\r\n" is followed by optional "field\r\n" lines
|
||||
// and a final "\r\n". chunk_remaining_ is repurposed as a flag: 1 = at start
|
||||
// of line (may be the empty terminator), 0 = mid-line (reading a trailer field).
|
||||
case ChunkedState::CHUNK_TRAILER:
|
||||
if (c == '\n') {
|
||||
if (this->chunk_remaining_ != 0) {
|
||||
this->chunk_state_ = ChunkedState::COMPLETE; // Empty line terminates trailers
|
||||
} else {
|
||||
this->chunk_remaining_ = 1; // End of trailer field, at start of next line
|
||||
}
|
||||
} else if (c != '\r') {
|
||||
this->chunk_remaining_ = 0; // Non-CRLF char: reading a trailer field
|
||||
}
|
||||
// \r doesn't change the flag — it's part of \r\n line endings
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->chunk_state_ == ChunkedState::COMPLETE) {
|
||||
// Clear chunked flag and set content_length to actual decoded size so
|
||||
// is_read_complete() returns true and callers exit their read loops
|
||||
this->is_chunked_ = false;
|
||||
this->content_length = this->bytes_read_;
|
||||
}
|
||||
}
|
||||
|
||||
return total_decoded;
|
||||
}
|
||||
|
||||
void HttpContainerArduino::end() {
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
this->client_.end();
|
||||
|
||||
@@ -18,6 +18,17 @@
|
||||
namespace esphome::http_request {
|
||||
|
||||
class HttpRequestArduino;
|
||||
|
||||
/// State machine for decoding chunked transfer encoding on Arduino
|
||||
enum class ChunkedState : uint8_t {
|
||||
CHUNK_HEADER, ///< Reading hex digits of chunk size
|
||||
CHUNK_HEADER_EXT, ///< Skipping chunk extensions until \n
|
||||
CHUNK_DATA, ///< Reading chunk data bytes
|
||||
CHUNK_DATA_TRAIL, ///< Skipping \r\n after chunk data
|
||||
CHUNK_TRAILER, ///< Consuming trailer headers after final 0-size chunk
|
||||
COMPLETE, ///< Finished: final chunk and trailers consumed
|
||||
};
|
||||
|
||||
class HttpContainerArduino : public HttpContainer {
|
||||
public:
|
||||
int read(uint8_t *buf, size_t max_len) override;
|
||||
@@ -26,6 +37,13 @@ class HttpContainerArduino : public HttpContainer {
|
||||
protected:
|
||||
friend class HttpRequestArduino;
|
||||
HTTPClient client_{};
|
||||
|
||||
/// Decode chunked transfer encoding from the raw stream
|
||||
int read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream);
|
||||
/// Transition from chunk header to data or trailer based on parsed size
|
||||
void chunk_header_complete_();
|
||||
ChunkedState chunk_state_{ChunkedState::CHUNK_HEADER};
|
||||
size_t chunk_remaining_{0}; ///< Bytes remaining in current chunk
|
||||
};
|
||||
|
||||
class HttpRequestArduino : public HttpRequestComponent {
|
||||
|
||||
@@ -133,8 +133,10 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||
// but this is defensive code in case chunked transfer encoding support is added for OTA in the future.
|
||||
// For non-chunked responses, COMPLETE is unreachable (loop condition checks bytes_read < content_length).
|
||||
// For chunked responses, the decoder sets content_length = bytes_read when the final chunk arrives,
|
||||
// which causes the loop condition to terminate. But COMPLETE can still be returned if the decoder
|
||||
// finishes mid-read, so this is needed for correctness.
|
||||
if (result == HttpReadLoopResult::COMPLETE)
|
||||
break;
|
||||
if (result != HttpReadLoopResult::DATA) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "light_state.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::light {
|
||||
|
||||
@@ -51,26 +52,13 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
|
||||
return *this; \
|
||||
}
|
||||
|
||||
// Color mode human-readable strings indexed by ColorModeBitPolicy::to_bit() (0-9)
|
||||
// Index 0 is Unknown (for ColorMode::UNKNOWN), also used as fallback for out-of-range
|
||||
PROGMEM_STRING_TABLE(ColorModeHumanStrings, "Unknown", "On/Off", "Brightness", "White", "Color temperature",
|
||||
"Cold/warm white", "RGB", "RGBW", "RGB + color temperature", "RGB + cold/warm white");
|
||||
|
||||
static const LogString *color_mode_to_human(ColorMode color_mode) {
|
||||
if (color_mode == ColorMode::ON_OFF)
|
||||
return LOG_STR("On/Off");
|
||||
if (color_mode == ColorMode::BRIGHTNESS)
|
||||
return LOG_STR("Brightness");
|
||||
if (color_mode == ColorMode::WHITE)
|
||||
return LOG_STR("White");
|
||||
if (color_mode == ColorMode::COLOR_TEMPERATURE)
|
||||
return LOG_STR("Color temperature");
|
||||
if (color_mode == ColorMode::COLD_WARM_WHITE)
|
||||
return LOG_STR("Cold/warm white");
|
||||
if (color_mode == ColorMode::RGB)
|
||||
return LOG_STR("RGB");
|
||||
if (color_mode == ColorMode::RGB_WHITE)
|
||||
return LOG_STR("RGBW");
|
||||
if (color_mode == ColorMode::RGB_COLD_WARM_WHITE)
|
||||
return LOG_STR("RGB + cold/warm white");
|
||||
if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE)
|
||||
return LOG_STR("RGB + color temperature");
|
||||
return LOG_STR("Unknown");
|
||||
return ColorModeHumanStrings::get_log_str(ColorModeBitPolicy::to_bit(color_mode), 0);
|
||||
}
|
||||
|
||||
// Helper to log percentage values
|
||||
@@ -457,6 +445,52 @@ ColorMode LightCall::compute_color_mode_() {
|
||||
LOG_STR_ARG(color_mode_to_human(color_mode)));
|
||||
return color_mode;
|
||||
}
|
||||
// PROGMEM lookup table for get_suitable_color_modes_mask_().
|
||||
// Maps 4-bit key (white | ct<<1 | cwww<<2 | rgb<<3) to color mode bitmask.
|
||||
// Packed into uint8_t by right-shifting by PACK_SHIFT since the lower bits
|
||||
// (UNKNOWN, ON_OFF, BRIGHTNESS) are never present in suitable mode masks.
|
||||
static constexpr unsigned PACK_SHIFT = ColorModeBitPolicy::to_bit(ColorMode::WHITE);
|
||||
// clang-format off
|
||||
static constexpr uint8_t SUITABLE_COLOR_MODES[] PROGMEM = {
|
||||
// [0] none - all modes with brightness
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [1] white only
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [2] ct only
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [3] white + ct
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [4] cwww only
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
0, // [5] white + cwww (conflicting)
|
||||
0, // [6] ct + cwww (conflicting)
|
||||
0, // [7] white + ct + cwww (conflicting)
|
||||
// [8] rgb only
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [9] rgb + white
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [10] rgb + ct
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [11] rgb + white + ct
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [12] rgb + cwww
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
0, // [13] rgb + white + cwww (conflicting)
|
||||
0, // [14] rgb + ct + cwww (conflicting)
|
||||
0, // [15] all (conflicting)
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
|
||||
bool has_white = this->has_white() && this->white_ > 0.0f;
|
||||
bool has_ct = this->has_color_temperature();
|
||||
@@ -466,46 +500,8 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
|
||||
(this->has_red() || this->has_green() || this->has_blue());
|
||||
|
||||
// Build key from flags: [rgb][cwww][ct][white]
|
||||
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
|
||||
|
||||
uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
|
||||
|
||||
switch (key) {
|
||||
case KEY(true, false, false, false): // white only
|
||||
return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, true, false, false): // ct only
|
||||
return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(true, true, false, false): // white + ct
|
||||
return ColorModeMask(
|
||||
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, false, true, false): // cwww only
|
||||
return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, false, false): // none
|
||||
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE,
|
||||
ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(true, false, false, true): // rgb + white
|
||||
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, true, false, true): // rgb + ct
|
||||
case KEY(true, true, false, true): // rgb + white + ct
|
||||
return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, true, true): // rgb + cwww
|
||||
return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, false, true): // rgb only
|
||||
return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
default:
|
||||
return 0; // conflicting flags
|
||||
}
|
||||
|
||||
#undef KEY
|
||||
uint8_t key = has_white | (has_ct << 1) | (has_cwww << 2) | (has_rgb << 3);
|
||||
return static_cast<color_mode_bitmask_t>(progmem_read_byte(&SUITABLE_COLOR_MODES[key])) << PACK_SHIFT;
|
||||
}
|
||||
|
||||
LightCall &LightCall::set_effect(const char *effect, size_t len) {
|
||||
|
||||
@@ -9,32 +9,19 @@ namespace esphome::light {
|
||||
|
||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||
|
||||
// Get JSON string for color mode.
|
||||
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would
|
||||
// generate a large jump table. Converting to bit index (0-9) allows a compact switch.
|
||||
// Color mode JSON strings - packed into flash with compile-time generated offsets.
|
||||
// Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff").
|
||||
PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct",
|
||||
"rgbww");
|
||||
|
||||
// Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0).
|
||||
// Returns ProgmemStr so ArduinoJson knows to handle PROGMEM strings on ESP8266.
|
||||
static ProgmemStr get_color_mode_json_str(ColorMode mode) {
|
||||
switch (ColorModeBitPolicy::to_bit(mode)) {
|
||||
case 1:
|
||||
return ESPHOME_F("onoff");
|
||||
case 2:
|
||||
return ESPHOME_F("brightness");
|
||||
case 3:
|
||||
return ESPHOME_F("white");
|
||||
case 4:
|
||||
return ESPHOME_F("color_temp");
|
||||
case 5:
|
||||
return ESPHOME_F("cwww");
|
||||
case 6:
|
||||
return ESPHOME_F("rgb");
|
||||
case 7:
|
||||
return ESPHOME_F("rgbw");
|
||||
case 8:
|
||||
return ESPHOME_F("rgbct");
|
||||
case 9:
|
||||
return ESPHOME_F("rgbww");
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
unsigned bit = ColorModeBitPolicy::to_bit(mode);
|
||||
if (bit == 0)
|
||||
return nullptr;
|
||||
// bit is 1-9 for valid modes, so bit-1 is always valid (0-8). LAST_INDEX fallback never used.
|
||||
return ColorModeStrings::get_progmem_str(bit - 1, ColorModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
|
||||
@@ -8,22 +8,12 @@ namespace esphome::lock {
|
||||
|
||||
static const char *const TAG = "lock";
|
||||
|
||||
// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING
|
||||
// Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range
|
||||
PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING");
|
||||
|
||||
const LogString *lock_state_to_string(LockState state) {
|
||||
switch (state) {
|
||||
case LOCK_STATE_LOCKED:
|
||||
return LOG_STR("LOCKED");
|
||||
case LOCK_STATE_UNLOCKED:
|
||||
return LOG_STR("UNLOCKED");
|
||||
case LOCK_STATE_JAMMED:
|
||||
return LOG_STR("JAMMED");
|
||||
case LOCK_STATE_LOCKING:
|
||||
return LOG_STR("LOCKING");
|
||||
case LOCK_STATE_UNLOCKING:
|
||||
return LOG_STR("UNLOCKING");
|
||||
case LOCK_STATE_NONE:
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return LockStateStrings::get_log_str(static_cast<uint8_t>(state), 0);
|
||||
}
|
||||
|
||||
Lock::Lock() : state(LOCK_STATE_NONE) {}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
@@ -241,34 +242,20 @@ UARTSelection Logger::get_uart() const { return this->uart_; }
|
||||
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// ESP8266: PSTR() cannot be used in array initializers, so we need to declare
|
||||
// each string separately as a global constant first
|
||||
static const char LOG_LEVEL_NONE[] PROGMEM = "NONE";
|
||||
static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR";
|
||||
static const char LOG_LEVEL_WARN[] PROGMEM = "WARN";
|
||||
static const char LOG_LEVEL_INFO[] PROGMEM = "INFO";
|
||||
static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG";
|
||||
static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG";
|
||||
static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE";
|
||||
static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE";
|
||||
// Log level strings - packed into flash on ESP8266, indexed by log level (0-7)
|
||||
PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE");
|
||||
|
||||
static const LogString *const LOG_LEVELS[] = {
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_NONE), reinterpret_cast<const LogString *>(LOG_LEVEL_ERROR),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_WARN), reinterpret_cast<const LogString *>(LOG_LEVEL_INFO),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_CONFIG), reinterpret_cast<const LogString *>(LOG_LEVEL_DEBUG),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_VERBOSE), reinterpret_cast<const LogString *>(LOG_LEVEL_VERY_VERBOSE),
|
||||
};
|
||||
#else
|
||||
static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
|
||||
#endif
|
||||
static const LogString *get_log_level_str(uint8_t level) {
|
||||
return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Logger:\n"
|
||||
" Max Level: %s\n"
|
||||
" Initial Level: %s",
|
||||
LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_]));
|
||||
LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)),
|
||||
LOG_STR_ARG(get_log_level_str(this->current_level_)));
|
||||
#ifndef USE_HOST
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Log Baud Rate: %" PRIu32 "\n"
|
||||
@@ -287,7 +274,7 @@ void Logger::dump_config() {
|
||||
|
||||
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
for (auto &it : this->log_levels_) {
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second]));
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second)));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -295,7 +282,8 @@ void Logger::dump_config() {
|
||||
void Logger::set_log_level(uint8_t level) {
|
||||
if (level > ESPHOME_LOG_LEVEL) {
|
||||
level = ESPHOME_LOG_LEVEL;
|
||||
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]));
|
||||
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s",
|
||||
LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)));
|
||||
}
|
||||
this->current_level_ = level;
|
||||
#ifdef USE_LOGGER_LEVEL_LISTENERS
|
||||
|
||||
@@ -13,31 +13,12 @@ static const char *const TAG = "mqtt.alarm_control_panel";
|
||||
|
||||
using namespace esphome::alarm_control_panel;
|
||||
|
||||
// Alarm state MQTT strings indexed by AlarmControlPanelState enum (0-9)
|
||||
PROGMEM_STRING_TABLE(AlarmMqttStateStrings, "disarmed", "armed_home", "armed_away", "armed_night", "armed_vacation",
|
||||
"armed_custom_bypass", "pending", "arming", "disarming", "triggered", "unknown");
|
||||
|
||||
static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) {
|
||||
switch (state) {
|
||||
case ACP_STATE_DISARMED:
|
||||
return ESPHOME_F("disarmed");
|
||||
case ACP_STATE_ARMED_HOME:
|
||||
return ESPHOME_F("armed_home");
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
return ESPHOME_F("armed_away");
|
||||
case ACP_STATE_ARMED_NIGHT:
|
||||
return ESPHOME_F("armed_night");
|
||||
case ACP_STATE_ARMED_VACATION:
|
||||
return ESPHOME_F("armed_vacation");
|
||||
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||
return ESPHOME_F("armed_custom_bypass");
|
||||
case ACP_STATE_PENDING:
|
||||
return ESPHOME_F("pending");
|
||||
case ACP_STATE_ARMING:
|
||||
return ESPHOME_F("arming");
|
||||
case ACP_STATE_DISARMING:
|
||||
return ESPHOME_F("disarming");
|
||||
case ACP_STATE_TRIGGERED:
|
||||
return ESPHOME_F("triggered");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return AlarmMqttStateStrings::get_progmem_str(static_cast<uint8_t>(state), AlarmMqttStateStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/version.h"
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
@@ -27,6 +28,11 @@ namespace esphome::mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt";
|
||||
|
||||
// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8)
|
||||
PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version",
|
||||
"Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized",
|
||||
"Not Enough Space", "TLS Bad Fingerprint", "DNS Resolve Error", "Unknown");
|
||||
|
||||
MQTTClientComponent::MQTTClientComponent() {
|
||||
global_mqtt_client = this;
|
||||
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
|
||||
@@ -348,36 +354,8 @@ void MQTTClientComponent::loop() {
|
||||
mqtt_backend_.loop();
|
||||
|
||||
if (this->disconnect_reason_.has_value()) {
|
||||
const LogString *reason_s;
|
||||
switch (*this->disconnect_reason_) {
|
||||
case MQTTClientDisconnectReason::TCP_DISCONNECTED:
|
||||
reason_s = LOG_STR("TCP disconnected");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
reason_s = LOG_STR("Unacceptable Protocol Version");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED:
|
||||
reason_s = LOG_STR("Identifier Rejected");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE:
|
||||
reason_s = LOG_STR("Server Unavailable");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS:
|
||||
reason_s = LOG_STR("Malformed Credentials");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED:
|
||||
reason_s = LOG_STR("Not Authorized");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE:
|
||||
reason_s = LOG_STR("Not Enough Space");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT:
|
||||
reason_s = LOG_STR("TLS Bad Fingerprint");
|
||||
break;
|
||||
default:
|
||||
reason_s = LOG_STR("Unknown");
|
||||
break;
|
||||
}
|
||||
const LogString *reason_s = MQTTDisconnectReasonStrings::get_log_str(
|
||||
static_cast<uint8_t>(*this->disconnect_reason_), MQTTDisconnectReasonStrings::LAST_INDEX);
|
||||
if (!network::is_connected()) {
|
||||
reason_s = LOG_STR("WiFi disconnected");
|
||||
}
|
||||
|
||||
@@ -13,109 +13,44 @@ static const char *const TAG = "mqtt.climate";
|
||||
|
||||
using namespace esphome::climate;
|
||||
|
||||
// Climate mode MQTT strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO
|
||||
PROGMEM_STRING_TABLE(ClimateMqttModeStrings, "off", "heat_cool", "cool", "heat", "fan_only", "dry", "auto", "unknown");
|
||||
|
||||
static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) {
|
||||
switch (mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_MODE_HEAT_COOL:
|
||||
return ESPHOME_F("heat_cool");
|
||||
case CLIMATE_MODE_AUTO:
|
||||
return ESPHOME_F("auto");
|
||||
case CLIMATE_MODE_COOL:
|
||||
return ESPHOME_F("cool");
|
||||
case CLIMATE_MODE_HEAT:
|
||||
return ESPHOME_F("heat");
|
||||
case CLIMATE_MODE_FAN_ONLY:
|
||||
return ESPHOME_F("fan_only");
|
||||
case CLIMATE_MODE_DRY:
|
||||
return ESPHOME_F("dry");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), ClimateMqttModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate action MQTT strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN
|
||||
PROGMEM_STRING_TABLE(ClimateMqttActionStrings, "off", "unknown", "cooling", "heating", "idle", "drying", "fan",
|
||||
"unknown");
|
||||
|
||||
static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) {
|
||||
switch (action) {
|
||||
case CLIMATE_ACTION_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_ACTION_COOLING:
|
||||
return ESPHOME_F("cooling");
|
||||
case CLIMATE_ACTION_HEATING:
|
||||
return ESPHOME_F("heating");
|
||||
case CLIMATE_ACTION_IDLE:
|
||||
return ESPHOME_F("idle");
|
||||
case CLIMATE_ACTION_DRYING:
|
||||
return ESPHOME_F("drying");
|
||||
case CLIMATE_ACTION_FAN:
|
||||
return ESPHOME_F("fan");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttActionStrings::get_progmem_str(static_cast<uint8_t>(action), ClimateMqttActionStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate fan mode MQTT strings indexed by ClimateFanMode enum (0-9)
|
||||
PROGMEM_STRING_TABLE(ClimateMqttFanModeStrings, "on", "off", "auto", "low", "medium", "high", "middle", "focus",
|
||||
"diffuse", "quiet", "unknown");
|
||||
|
||||
static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) {
|
||||
switch (fan_mode) {
|
||||
case CLIMATE_FAN_ON:
|
||||
return ESPHOME_F("on");
|
||||
case CLIMATE_FAN_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_FAN_AUTO:
|
||||
return ESPHOME_F("auto");
|
||||
case CLIMATE_FAN_LOW:
|
||||
return ESPHOME_F("low");
|
||||
case CLIMATE_FAN_MEDIUM:
|
||||
return ESPHOME_F("medium");
|
||||
case CLIMATE_FAN_HIGH:
|
||||
return ESPHOME_F("high");
|
||||
case CLIMATE_FAN_MIDDLE:
|
||||
return ESPHOME_F("middle");
|
||||
case CLIMATE_FAN_FOCUS:
|
||||
return ESPHOME_F("focus");
|
||||
case CLIMATE_FAN_DIFFUSE:
|
||||
return ESPHOME_F("diffuse");
|
||||
case CLIMATE_FAN_QUIET:
|
||||
return ESPHOME_F("quiet");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttFanModeStrings::get_progmem_str(static_cast<uint8_t>(fan_mode),
|
||||
ClimateMqttFanModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate swing mode MQTT strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL
|
||||
PROGMEM_STRING_TABLE(ClimateMqttSwingModeStrings, "off", "both", "vertical", "horizontal", "unknown");
|
||||
|
||||
static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) {
|
||||
switch (swing_mode) {
|
||||
case CLIMATE_SWING_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_SWING_BOTH:
|
||||
return ESPHOME_F("both");
|
||||
case CLIMATE_SWING_VERTICAL:
|
||||
return ESPHOME_F("vertical");
|
||||
case CLIMATE_SWING_HORIZONTAL:
|
||||
return ESPHOME_F("horizontal");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttSwingModeStrings::get_progmem_str(static_cast<uint8_t>(swing_mode),
|
||||
ClimateMqttSwingModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// Climate preset MQTT strings indexed by ClimatePreset enum (0-7)
|
||||
PROGMEM_STRING_TABLE(ClimateMqttPresetStrings, "none", "home", "away", "boost", "comfort", "eco", "sleep", "activity",
|
||||
"unknown");
|
||||
|
||||
static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) {
|
||||
switch (preset) {
|
||||
case CLIMATE_PRESET_NONE:
|
||||
return ESPHOME_F("none");
|
||||
case CLIMATE_PRESET_HOME:
|
||||
return ESPHOME_F("home");
|
||||
case CLIMATE_PRESET_ECO:
|
||||
return ESPHOME_F("eco");
|
||||
case CLIMATE_PRESET_AWAY:
|
||||
return ESPHOME_F("away");
|
||||
case CLIMATE_PRESET_BOOST:
|
||||
return ESPHOME_F("boost");
|
||||
case CLIMATE_PRESET_COMFORT:
|
||||
return ESPHOME_F("comfort");
|
||||
case CLIMATE_PRESET_SLEEP:
|
||||
return ESPHOME_F("sleep");
|
||||
case CLIMATE_PRESET_ACTIVITY:
|
||||
return ESPHOME_F("activity");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
return ClimateMqttPresetStrings::get_progmem_str(static_cast<uint8_t>(preset), ClimateMqttPresetStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace esphome::mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt.component";
|
||||
|
||||
// Entity category MQTT strings indexed by EntityCategory enum: NONE(0) is skipped, CONFIG(1), DIAGNOSTIC(2)
|
||||
PROGMEM_STRING_TABLE(EntityCategoryMqttStrings, "", "config", "diagnostic");
|
||||
|
||||
// Helper functions for building topic strings on stack
|
||||
inline char *append_str(char *p, const char *s, size_t len) {
|
||||
memcpy(p, s, len);
|
||||
@@ -213,13 +216,9 @@ bool MQTTComponent::send_discovery_() {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
const auto entity_category = this->get_entity()->get_entity_category();
|
||||
switch (entity_category) {
|
||||
case ENTITY_CATEGORY_NONE:
|
||||
break;
|
||||
case ENTITY_CATEGORY_CONFIG:
|
||||
case ENTITY_CATEGORY_DIAGNOSTIC:
|
||||
root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
|
||||
break;
|
||||
if (entity_category != ENTITY_CATEGORY_NONE) {
|
||||
root[MQTT_ENTITY_CATEGORY] = EntityCategoryMqttStrings::get_progmem_str(
|
||||
static_cast<uint8_t>(entity_category), static_cast<uint8_t>(ENTITY_CATEGORY_CONFIG));
|
||||
}
|
||||
|
||||
if (config.state_topic) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "mqtt_number.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.number";
|
||||
|
||||
using namespace esphome::number;
|
||||
|
||||
// Number mode MQTT strings indexed by NumberMode enum: AUTO(0) is skipped, BOX(1), SLIDER(2)
|
||||
PROGMEM_STRING_TABLE(NumberMqttModeStrings, "", "box", "slider");
|
||||
|
||||
MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {}
|
||||
|
||||
void MQTTNumberComponent::setup() {
|
||||
@@ -48,15 +52,10 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
if (!unit_of_measurement.empty()) {
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
|
||||
}
|
||||
switch (this->number_->traits.get_mode()) {
|
||||
case NUMBER_MODE_AUTO:
|
||||
break;
|
||||
case NUMBER_MODE_BOX:
|
||||
root[MQTT_MODE] = "box";
|
||||
break;
|
||||
case NUMBER_MODE_SLIDER:
|
||||
root[MQTT_MODE] = "slider";
|
||||
break;
|
||||
const auto mode = this->number_->traits.get_mode();
|
||||
if (mode != NUMBER_MODE_AUTO) {
|
||||
root[MQTT_MODE] =
|
||||
NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX));
|
||||
}
|
||||
const auto device_class = this->number_->traits.get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "mqtt_text.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.text";
|
||||
|
||||
using namespace esphome::text;
|
||||
|
||||
// Text mode MQTT strings indexed by TextMode enum (0-1): TEXT, PASSWORD
|
||||
PROGMEM_STRING_TABLE(TextMqttModeStrings, "text", "password");
|
||||
|
||||
MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {}
|
||||
|
||||
void MQTTTextComponent::setup() {
|
||||
@@ -34,14 +38,8 @@ const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
||||
|
||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
switch (this->text_->traits.get_mode()) {
|
||||
case TEXT_MODE_TEXT:
|
||||
root[MQTT_MODE] = "text";
|
||||
break;
|
||||
case TEXT_MODE_PASSWORD:
|
||||
root[MQTT_MODE] = "password";
|
||||
break;
|
||||
}
|
||||
root[MQTT_MODE] = TextMqttModeStrings::get_progmem_str(static_cast<uint8_t>(this->text_->traits.get_mode()),
|
||||
static_cast<uint8_t>(TEXT_MODE_TEXT));
|
||||
|
||||
config.command_topic = true;
|
||||
}
|
||||
|
||||
@@ -56,17 +56,23 @@ void PylontechComponent::setup() {
|
||||
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
||||
|
||||
void PylontechComponent::loop() {
|
||||
if (this->available() > 0) {
|
||||
int avail = this->available();
|
||||
if (avail > 0) {
|
||||
// pylontech sends a lot of data very suddenly
|
||||
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
|
||||
uint8_t data;
|
||||
int recv = 0;
|
||||
while (this->available() > 0) {
|
||||
if (this->read_byte(&data)) {
|
||||
buffer_[buffer_index_write_] += (char) data;
|
||||
recv++;
|
||||
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
|
||||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
recv += to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
buffer_[buffer_index_write_] += (char) buf[i];
|
||||
if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
// complete line received
|
||||
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::sensor {
|
||||
|
||||
@@ -30,20 +31,13 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o
|
||||
}
|
||||
}
|
||||
|
||||
// State class strings indexed by StateClass enum (0-4): NONE, MEASUREMENT, TOTAL_INCREASING, TOTAL, MEASUREMENT_ANGLE
|
||||
PROGMEM_STRING_TABLE(StateClassStrings, "", "measurement", "total_increasing", "total", "measurement_angle");
|
||||
static_assert(StateClassStrings::COUNT == STATE_CLASS_LAST + 1, "StateClassStrings must match StateClass enum");
|
||||
|
||||
const LogString *state_class_to_string(StateClass state_class) {
|
||||
switch (state_class) {
|
||||
case STATE_CLASS_MEASUREMENT:
|
||||
return LOG_STR("measurement");
|
||||
case STATE_CLASS_TOTAL_INCREASING:
|
||||
return LOG_STR("total_increasing");
|
||||
case STATE_CLASS_TOTAL:
|
||||
return LOG_STR("total");
|
||||
case STATE_CLASS_MEASUREMENT_ANGLE:
|
||||
return LOG_STR("measurement_angle");
|
||||
case STATE_CLASS_NONE:
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
// Fallback to index 0 (empty string for STATE_CLASS_NONE) if out of range
|
||||
return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0);
|
||||
}
|
||||
|
||||
Sensor::Sensor() : state(NAN), raw_state(NAN) {}
|
||||
|
||||
@@ -32,6 +32,7 @@ enum StateClass : uint8_t {
|
||||
STATE_CLASS_TOTAL = 3,
|
||||
STATE_CLASS_MEASUREMENT_ANGLE = 4
|
||||
};
|
||||
constexpr uint8_t STATE_CLASS_LAST = static_cast<uint8_t>(STATE_CLASS_MEASUREMENT_ANGLE);
|
||||
|
||||
const LogString *state_class_to_string(StateClass state_class);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::template_ {
|
||||
|
||||
@@ -28,18 +29,11 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
|
||||
this->sensor_data_.push_back(sd);
|
||||
};
|
||||
|
||||
// Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS
|
||||
PROGMEM_STRING_TABLE(AlarmSensorTypeStrings, "delayed", "instant", "delayed_follower", "instant_always");
|
||||
|
||||
static const LogString *sensor_type_to_string(AlarmSensorType type) {
|
||||
switch (type) {
|
||||
case ALARM_SENSOR_TYPE_INSTANT:
|
||||
return LOG_STR("instant");
|
||||
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
|
||||
return LOG_STR("delayed_follower");
|
||||
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
|
||||
return LOG_STR("instant_always");
|
||||
case ALARM_SENSOR_TYPE_DELAYED:
|
||||
default:
|
||||
return LOG_STR("delayed");
|
||||
}
|
||||
return AlarmSensorTypeStrings::get_log_str(static_cast<uint8_t>(type), 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ enum BinarySensorFlags : uint16_t {
|
||||
BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4,
|
||||
};
|
||||
|
||||
enum AlarmSensorType : uint16_t {
|
||||
enum AlarmSensorType : uint8_t {
|
||||
ALARM_SENSOR_TYPE_DELAYED = 0,
|
||||
ALARM_SENSOR_TYPE_INSTANT,
|
||||
ALARM_SENSOR_TYPE_DELAYED_FOLLOWER,
|
||||
|
||||
@@ -2,12 +2,21 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace update {
|
||||
|
||||
static const char *const TAG = "update";
|
||||
|
||||
// Update state strings indexed by UpdateState enum (0-3): UNKNOWN, NO UPDATE, UPDATE AVAILABLE, INSTALLING
|
||||
PROGMEM_STRING_TABLE(UpdateStateStrings, "UNKNOWN", "NO UPDATE", "UPDATE AVAILABLE", "INSTALLING");
|
||||
|
||||
const LogString *update_state_to_string(UpdateState state) {
|
||||
return UpdateStateStrings::get_log_str(static_cast<uint8_t>(state),
|
||||
static_cast<uint8_t>(UpdateState::UPDATE_STATE_UNKNOWN));
|
||||
}
|
||||
|
||||
void UpdateEntity::publish_state() {
|
||||
ESP_LOGD(TAG,
|
||||
"'%s' >>\n"
|
||||
|
||||
@@ -27,6 +27,8 @@ enum UpdateState : uint8_t {
|
||||
UPDATE_STATE_INSTALLING,
|
||||
};
|
||||
|
||||
const LogString *update_state_to_string(UpdateState state);
|
||||
|
||||
class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
|
||||
public:
|
||||
void publish_state();
|
||||
|
||||
@@ -23,17 +23,11 @@ const LogString *valve_command_to_str(float pos) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
// Valve operation strings indexed by ValveOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN
|
||||
PROGMEM_STRING_TABLE(ValveOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN");
|
||||
|
||||
const LogString *valve_operation_to_str(ValveOperation op) {
|
||||
switch (op) {
|
||||
case VALVE_OPERATION_IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case VALVE_OPERATION_OPENING:
|
||||
return LOG_STR("OPENING");
|
||||
case VALVE_OPERATION_CLOSING:
|
||||
return LOG_STR("CLOSING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return ValveOperationStrings::get_log_str(static_cast<uint8_t>(op), ValveOperationStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
Valve::Valve() : position{VALVE_OPEN} {}
|
||||
|
||||
@@ -233,25 +233,13 @@ void WaterHeater::set_visual_target_temperature_step_override(float visual_targe
|
||||
}
|
||||
#endif
|
||||
|
||||
// Water heater mode strings indexed by WaterHeaterMode enum (0-6): OFF, ECO, ELECTRIC, PERFORMANCE, HIGH_DEMAND,
|
||||
// HEAT_PUMP, GAS
|
||||
PROGMEM_STRING_TABLE(WaterHeaterModeStrings, "OFF", "ECO", "ELECTRIC", "PERFORMANCE", "HIGH_DEMAND", "HEAT_PUMP", "GAS",
|
||||
"UNKNOWN");
|
||||
|
||||
const LogString *water_heater_mode_to_string(WaterHeaterMode mode) {
|
||||
switch (mode) {
|
||||
case WATER_HEATER_MODE_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case WATER_HEATER_MODE_ECO:
|
||||
return LOG_STR("ECO");
|
||||
case WATER_HEATER_MODE_ELECTRIC:
|
||||
return LOG_STR("ELECTRIC");
|
||||
case WATER_HEATER_MODE_PERFORMANCE:
|
||||
return LOG_STR("PERFORMANCE");
|
||||
case WATER_HEATER_MODE_HIGH_DEMAND:
|
||||
return LOG_STR("HIGH_DEMAND");
|
||||
case WATER_HEATER_MODE_HEAT_PUMP:
|
||||
return LOG_STR("HEAT_PUMP");
|
||||
case WATER_HEATER_MODE_GAS:
|
||||
return LOG_STR("GAS");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return WaterHeaterModeStrings::get_log_str(static_cast<uint8_t>(mode), WaterHeaterModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void WaterHeater::dump_traits_(const char *tag) {
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
#include "esphome/components/update/update_entity.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
@@ -2104,19 +2108,6 @@ std::string WebServer::event_json_(event::Event *obj, StringRef event_type, Json
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
static const LogString *update_state_to_string(update::UpdateState state) {
|
||||
switch (state) {
|
||||
case update::UPDATE_STATE_NO_UPDATE:
|
||||
return LOG_STR("NO UPDATE");
|
||||
case update::UPDATE_STATE_AVAILABLE:
|
||||
return LOG_STR("UPDATE AVAILABLE");
|
||||
case update::UPDATE_STATE_INSTALLING:
|
||||
return LOG_STR("INSTALLING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
void WebServer::on_update(update::UpdateEntity *obj) {
|
||||
this->events_.deferrable_send_state(obj, "state", update_state_json_generator);
|
||||
}
|
||||
@@ -2158,7 +2149,7 @@ std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_
|
||||
JsonObject root = builder.root();
|
||||
|
||||
char buf[PSTR_LOCAL_SIZE];
|
||||
set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update_state_to_string(obj->state)),
|
||||
set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update::update_state_to_string(obj->state)),
|
||||
obj->update_info.latest_version, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root[ESPHOME_F("current_version")] = obj->update_info.current_version;
|
||||
|
||||
@@ -236,25 +236,23 @@ static const char *const TAG = "wifi";
|
||||
/// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │
|
||||
/// └──────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
|
||||
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
|
||||
switch (phase) {
|
||||
case WiFiRetryPhase::INITIAL_CONNECT:
|
||||
return LOG_STR("INITIAL_CONNECT");
|
||||
if (phase == WiFiRetryPhase::INITIAL_CONNECT)
|
||||
return LOG_STR("INITIAL_CONNECT");
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS:
|
||||
return LOG_STR("FAST_CONNECT_CYCLING");
|
||||
if (phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS)
|
||||
return LOG_STR("FAST_CONNECT_CYCLING");
|
||||
#endif
|
||||
case WiFiRetryPhase::EXPLICIT_HIDDEN:
|
||||
return LOG_STR("EXPLICIT_HIDDEN");
|
||||
case WiFiRetryPhase::SCAN_CONNECTING:
|
||||
return LOG_STR("SCAN_CONNECTING");
|
||||
case WiFiRetryPhase::RETRY_HIDDEN:
|
||||
return LOG_STR("RETRY_HIDDEN");
|
||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||
return LOG_STR("RESTARTING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
if (phase == WiFiRetryPhase::EXPLICIT_HIDDEN)
|
||||
return LOG_STR("EXPLICIT_HIDDEN");
|
||||
if (phase == WiFiRetryPhase::SCAN_CONNECTING)
|
||||
return LOG_STR("SCAN_CONNECTING");
|
||||
if (phase == WiFiRetryPhase::RETRY_HIDDEN)
|
||||
return LOG_STR("RETRY_HIDDEN");
|
||||
if (phase == WiFiRetryPhase::RESTARTING_ADAPTER)
|
||||
return LOG_STR("RESTARTING");
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
bool WiFiComponent::went_through_explicit_hidden_phase_() const {
|
||||
@@ -1470,6 +1468,14 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
this->notify_connect_state_listeners_();
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
// On ESP8266, GOT_IP event may not fire for static IP configurations,
|
||||
// so notify IP state listeners here as a fallback.
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
this->notify_ip_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1481,7 +1487,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
}
|
||||
|
||||
if (this->error_from_callback_) {
|
||||
// ESP8266: logging done in callback, listeners deferred via pending_.disconnect
|
||||
// Other platforms: just log generic failure message
|
||||
#ifndef USE_ESP8266
|
||||
ESP_LOGW(TAG, "Connecting to network failed (callback)");
|
||||
#endif
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
@@ -2202,8 +2212,31 @@ void WiFiComponent::notify_connect_state_listeners_() {
|
||||
listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid);
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiComponent::notify_disconnect_state_listeners_() {
|
||||
constexpr uint8_t empty_bssid[6] = {};
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), empty_bssid);
|
||||
}
|
||||
}
|
||||
#endif // USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
void WiFiComponent::notify_ip_state_listeners_() {
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif // USE_WIFI_IP_STATE_LISTENERS
|
||||
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
void WiFiComponent::notify_scan_results_listeners_() {
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
}
|
||||
}
|
||||
#endif // USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
|
||||
void WiFiComponent::check_roaming_(uint32_t now) {
|
||||
// Guard: not for hidden networks (may not appear in scan)
|
||||
const WiFiAP *selected = this->get_selected_sta_();
|
||||
|
||||
@@ -594,6 +594,9 @@ class WiFiComponent : public Component {
|
||||
void connect_soon_();
|
||||
|
||||
void wifi_loop_();
|
||||
#ifdef USE_ESP8266
|
||||
void process_pending_callbacks_();
|
||||
#endif
|
||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||
bool wifi_sta_pre_setup_();
|
||||
bool wifi_apply_output_power_(float output_power);
|
||||
@@ -635,6 +638,16 @@ class WiFiComponent : public Component {
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
/// Notify connect state listeners (called after state machine reaches STA_CONNECTED)
|
||||
void notify_connect_state_listeners_();
|
||||
/// Notify connect state listeners of disconnection
|
||||
void notify_disconnect_state_listeners_();
|
||||
#endif
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
/// Notify IP state listeners with current addresses
|
||||
void notify_ip_state_listeners_();
|
||||
#endif
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
/// Notify scan results listeners with current scan results
|
||||
void notify_scan_results_listeners_();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
@@ -658,13 +671,13 @@ class WiFiComponent : public Component {
|
||||
void wifi_scan_done_callback_();
|
||||
#endif
|
||||
|
||||
// Large/pointer-aligned members first
|
||||
FixedVector<WiFiAP> sta_;
|
||||
std::vector<WiFiSTAPriority> sta_priorities_;
|
||||
wifi_scan_vector_t<WiFiScanResult> scan_result_;
|
||||
#ifdef USE_WIFI_AP
|
||||
WiFiAP ap_;
|
||||
#endif
|
||||
float output_power_{NAN};
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
StaticVector<WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS> ip_state_listeners_;
|
||||
#endif
|
||||
@@ -681,6 +694,15 @@ class WiFiComponent : public Component {
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
ESPPreferenceObject fast_connect_pref_;
|
||||
#endif
|
||||
#ifdef USE_WIFI_CONNECT_TRIGGER
|
||||
Trigger<> connect_trigger_;
|
||||
#endif
|
||||
#ifdef USE_WIFI_DISCONNECT_TRIGGER
|
||||
Trigger<> disconnect_trigger_;
|
||||
#endif
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
SemaphoreHandle_t high_performance_semaphore_{nullptr};
|
||||
#endif
|
||||
|
||||
// Post-connect roaming constants
|
||||
static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
@@ -688,7 +710,8 @@ class WiFiComponent : public Component {
|
||||
static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent
|
||||
static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3;
|
||||
|
||||
// Group all 32-bit integers together
|
||||
// 4-byte members
|
||||
float output_power_{NAN};
|
||||
uint32_t action_started_;
|
||||
uint32_t last_connected_{0};
|
||||
uint32_t reboot_timeout_{};
|
||||
@@ -697,7 +720,7 @@ class WiFiComponent : public Component {
|
||||
uint32_t ap_timeout_{};
|
||||
#endif
|
||||
|
||||
// Group all 8-bit values together
|
||||
// 1-byte enums and integers
|
||||
WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
|
||||
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
|
||||
WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2};
|
||||
@@ -708,17 +731,39 @@ class WiFiComponent : public Component {
|
||||
// int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS)
|
||||
int8_t selected_sta_index_{-1};
|
||||
uint8_t roaming_attempts_{0};
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
uint8_t num_ipv6_addresses_{0};
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
bool error_from_callback_{false};
|
||||
RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY};
|
||||
RoamingState roaming_state_{RoamingState::IDLE};
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE};
|
||||
#endif
|
||||
|
||||
// Group all boolean values together
|
||||
// Bools and bitfields
|
||||
// Pending listener callbacks deferred from platform callbacks to main loop.
|
||||
struct {
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
// Deferred until state machine reaches STA_CONNECTED so wifi.connected
|
||||
// condition returns true in listener automations.
|
||||
bool connect_state : 1;
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266: also defer disconnect notification to main loop
|
||||
bool disconnect : 1;
|
||||
#endif
|
||||
#endif
|
||||
#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS)
|
||||
bool got_ip : 1;
|
||||
#endif
|
||||
#if defined(USE_ESP8266) && defined(USE_WIFI_SCAN_RESULTS_LISTENERS)
|
||||
bool scan_complete : 1;
|
||||
#endif
|
||||
} pending_{};
|
||||
bool has_ap_{false};
|
||||
#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)
|
||||
bool handled_connected_state_{false};
|
||||
#endif
|
||||
bool error_from_callback_{false};
|
||||
bool scan_done_{false};
|
||||
bool ap_setup_{false};
|
||||
bool ap_started_{false};
|
||||
@@ -733,32 +778,10 @@ class WiFiComponent : public Component {
|
||||
bool keep_scan_results_{false};
|
||||
bool has_completed_scan_after_captive_portal_start_{
|
||||
false}; // Tracks if we've completed a scan after captive portal started
|
||||
RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY};
|
||||
bool skip_cooldown_next_cycle_{false};
|
||||
bool post_connect_roaming_{true}; // Enabled by default
|
||||
RoamingState roaming_state_{RoamingState::IDLE};
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE};
|
||||
bool is_high_performance_mode_{false};
|
||||
|
||||
SemaphoreHandle_t high_performance_semaphore_{nullptr};
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
// Pending listener notifications deferred until state machine reaches appropriate state.
|
||||
// Listeners are notified after state transitions complete so conditions like
|
||||
// wifi.connected return correct values in automations.
|
||||
// Uses bitfields to minimize memory; more flags may be added as needed.
|
||||
struct {
|
||||
bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED
|
||||
} pending_{};
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_CONNECT_TRIGGER
|
||||
Trigger<> connect_trigger_;
|
||||
#endif
|
||||
#ifdef USE_WIFI_DISCONNECT_TRIGGER
|
||||
Trigger<> disconnect_trigger_;
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
@@ -36,6 +36,7 @@ extern "C" {
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/util.h"
|
||||
|
||||
namespace esphome::wifi {
|
||||
@@ -398,106 +399,82 @@ class WiFiMockClass : public ESP8266WiFiGenericClass {
|
||||
static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT
|
||||
};
|
||||
|
||||
// Auth mode strings indexed by AUTH_* constants (0-4), with UNKNOWN at last index
|
||||
// Static asserts verify the SDK constants are contiguous as expected
|
||||
static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4,
|
||||
"AUTH_* constants are not contiguous");
|
||||
PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN");
|
||||
|
||||
const LogString *get_auth_mode_str(uint8_t mode) {
|
||||
switch (mode) {
|
||||
case AUTH_OPEN:
|
||||
return LOG_STR("OPEN");
|
||||
case AUTH_WEP:
|
||||
return LOG_STR("WEP");
|
||||
case AUTH_WPA_PSK:
|
||||
return LOG_STR("WPA PSK");
|
||||
case AUTH_WPA2_PSK:
|
||||
return LOG_STR("WPA2 PSK");
|
||||
case AUTH_WPA_WPA2_PSK:
|
||||
return LOG_STR("WPA/WPA2 PSK");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
const LogString *get_op_mode_str(uint8_t mode) {
|
||||
switch (mode) {
|
||||
case WIFI_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case WIFI_STA:
|
||||
return LOG_STR("STA");
|
||||
case WIFI_AP:
|
||||
return LOG_STR("AP");
|
||||
case WIFI_AP_STA:
|
||||
return LOG_STR("AP+STA");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return AuthModeStrings::get_log_str(mode, AuthModeStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at last index
|
||||
static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3,
|
||||
"WIFI_* op mode constants are not contiguous");
|
||||
PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN");
|
||||
|
||||
const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); }
|
||||
|
||||
// Use if-chain instead of switch to avoid jump tables in RODATA (wastes RAM on ESP8266).
|
||||
// A single switch would generate a sparse lookup table with ~175 default entries, wasting 700 bytes of RAM.
|
||||
// Even split switches still generate smaller jump tables in RODATA.
|
||||
const LogString *get_disconnect_reason_str(uint8_t reason) {
|
||||
/* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the
|
||||
* REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM
|
||||
* per entry. As there's ~175 default entries, this wastes 700 bytes of RAM.
|
||||
*/
|
||||
if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200
|
||||
switch (reason) {
|
||||
case REASON_AUTH_EXPIRE:
|
||||
return LOG_STR("Auth Expired");
|
||||
case REASON_AUTH_LEAVE:
|
||||
return LOG_STR("Auth Leave");
|
||||
case REASON_ASSOC_EXPIRE:
|
||||
return LOG_STR("Association Expired");
|
||||
case REASON_ASSOC_TOOMANY:
|
||||
return LOG_STR("Too Many Associations");
|
||||
case REASON_NOT_AUTHED:
|
||||
return LOG_STR("Not Authenticated");
|
||||
case REASON_NOT_ASSOCED:
|
||||
return LOG_STR("Not Associated");
|
||||
case REASON_ASSOC_LEAVE:
|
||||
return LOG_STR("Association Leave");
|
||||
case REASON_ASSOC_NOT_AUTHED:
|
||||
return LOG_STR("Association not Authenticated");
|
||||
case REASON_DISASSOC_PWRCAP_BAD:
|
||||
return LOG_STR("Disassociate Power Cap Bad");
|
||||
case REASON_DISASSOC_SUPCHAN_BAD:
|
||||
return LOG_STR("Disassociate Supported Channel Bad");
|
||||
case REASON_IE_INVALID:
|
||||
return LOG_STR("IE Invalid");
|
||||
case REASON_MIC_FAILURE:
|
||||
return LOG_STR("Mic Failure");
|
||||
case REASON_4WAY_HANDSHAKE_TIMEOUT:
|
||||
return LOG_STR("4-Way Handshake Timeout");
|
||||
case REASON_GROUP_KEY_UPDATE_TIMEOUT:
|
||||
return LOG_STR("Group Key Update Timeout");
|
||||
case REASON_IE_IN_4WAY_DIFFERS:
|
||||
return LOG_STR("IE In 4-Way Handshake Differs");
|
||||
case REASON_GROUP_CIPHER_INVALID:
|
||||
return LOG_STR("Group Cipher Invalid");
|
||||
case REASON_PAIRWISE_CIPHER_INVALID:
|
||||
return LOG_STR("Pairwise Cipher Invalid");
|
||||
case REASON_AKMP_INVALID:
|
||||
return LOG_STR("AKMP Invalid");
|
||||
case REASON_UNSUPP_RSN_IE_VERSION:
|
||||
return LOG_STR("Unsupported RSN IE version");
|
||||
case REASON_INVALID_RSN_IE_CAP:
|
||||
return LOG_STR("Invalid RSN IE Cap");
|
||||
case REASON_802_1X_AUTH_FAILED:
|
||||
return LOG_STR("802.1x Authentication Failed");
|
||||
case REASON_CIPHER_SUITE_REJECTED:
|
||||
return LOG_STR("Cipher Suite Rejected");
|
||||
}
|
||||
}
|
||||
|
||||
switch (reason) {
|
||||
case REASON_BEACON_TIMEOUT:
|
||||
return LOG_STR("Beacon Timeout");
|
||||
case REASON_NO_AP_FOUND:
|
||||
return LOG_STR("AP Not Found");
|
||||
case REASON_AUTH_FAIL:
|
||||
return LOG_STR("Authentication Failed");
|
||||
case REASON_ASSOC_FAIL:
|
||||
return LOG_STR("Association Failed");
|
||||
case REASON_HANDSHAKE_TIMEOUT:
|
||||
return LOG_STR("Handshake Failed");
|
||||
case REASON_UNSPECIFIED:
|
||||
default:
|
||||
return LOG_STR("Unspecified");
|
||||
}
|
||||
if (reason == REASON_AUTH_EXPIRE)
|
||||
return LOG_STR("Auth Expired");
|
||||
if (reason == REASON_AUTH_LEAVE)
|
||||
return LOG_STR("Auth Leave");
|
||||
if (reason == REASON_ASSOC_EXPIRE)
|
||||
return LOG_STR("Association Expired");
|
||||
if (reason == REASON_ASSOC_TOOMANY)
|
||||
return LOG_STR("Too Many Associations");
|
||||
if (reason == REASON_NOT_AUTHED)
|
||||
return LOG_STR("Not Authenticated");
|
||||
if (reason == REASON_NOT_ASSOCED)
|
||||
return LOG_STR("Not Associated");
|
||||
if (reason == REASON_ASSOC_LEAVE)
|
||||
return LOG_STR("Association Leave");
|
||||
if (reason == REASON_ASSOC_NOT_AUTHED)
|
||||
return LOG_STR("Association not Authenticated");
|
||||
if (reason == REASON_DISASSOC_PWRCAP_BAD)
|
||||
return LOG_STR("Disassociate Power Cap Bad");
|
||||
if (reason == REASON_DISASSOC_SUPCHAN_BAD)
|
||||
return LOG_STR("Disassociate Supported Channel Bad");
|
||||
if (reason == REASON_IE_INVALID)
|
||||
return LOG_STR("IE Invalid");
|
||||
if (reason == REASON_MIC_FAILURE)
|
||||
return LOG_STR("Mic Failure");
|
||||
if (reason == REASON_4WAY_HANDSHAKE_TIMEOUT)
|
||||
return LOG_STR("4-Way Handshake Timeout");
|
||||
if (reason == REASON_GROUP_KEY_UPDATE_TIMEOUT)
|
||||
return LOG_STR("Group Key Update Timeout");
|
||||
if (reason == REASON_IE_IN_4WAY_DIFFERS)
|
||||
return LOG_STR("IE In 4-Way Handshake Differs");
|
||||
if (reason == REASON_GROUP_CIPHER_INVALID)
|
||||
return LOG_STR("Group Cipher Invalid");
|
||||
if (reason == REASON_PAIRWISE_CIPHER_INVALID)
|
||||
return LOG_STR("Pairwise Cipher Invalid");
|
||||
if (reason == REASON_AKMP_INVALID)
|
||||
return LOG_STR("AKMP Invalid");
|
||||
if (reason == REASON_UNSUPP_RSN_IE_VERSION)
|
||||
return LOG_STR("Unsupported RSN IE version");
|
||||
if (reason == REASON_INVALID_RSN_IE_CAP)
|
||||
return LOG_STR("Invalid RSN IE Cap");
|
||||
if (reason == REASON_802_1X_AUTH_FAILED)
|
||||
return LOG_STR("802.1x Authentication Failed");
|
||||
if (reason == REASON_CIPHER_SUITE_REJECTED)
|
||||
return LOG_STR("Cipher Suite Rejected");
|
||||
if (reason == REASON_BEACON_TIMEOUT)
|
||||
return LOG_STR("Beacon Timeout");
|
||||
if (reason == REASON_NO_AP_FOUND)
|
||||
return LOG_STR("AP Not Found");
|
||||
if (reason == REASON_AUTH_FAIL)
|
||||
return LOG_STR("Authentication Failed");
|
||||
if (reason == REASON_ASSOC_FAIL)
|
||||
return LOG_STR("Association Failed");
|
||||
if (reason == REASON_HANDSHAKE_TIMEOUT)
|
||||
return LOG_STR("Handshake Failed");
|
||||
return LOG_STR("Unspecified");
|
||||
}
|
||||
|
||||
// TODO: This callback runs in ESP8266 system context with limited stack (~2KB).
|
||||
@@ -519,16 +496,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
// Defer listener notification until state machine reaches STA_CONNECTED
|
||||
// This ensures wifi.connected condition returns true in listener automations
|
||||
global_wifi_component->pending_.connect_state = true;
|
||||
#endif
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
if (const WiFiAP *config = global_wifi_component->get_selected_sta_();
|
||||
config && config->get_manual_ip().has_value()) {
|
||||
for (auto *listener : global_wifi_component->ip_state_listeners_) {
|
||||
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(),
|
||||
global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -547,16 +514,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
}
|
||||
s_sta_connected = false;
|
||||
s_sta_connecting = false;
|
||||
// IMPORTANT: Set error flag BEFORE notifying listeners.
|
||||
// This ensures is_connected() returns false during listener callbacks,
|
||||
// which is critical for proper reconnection logic (e.g., roaming).
|
||||
global_wifi_component->error_from_callback_ = true;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
// Notify listeners AFTER setting error flag so they see correct state
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : global_wifi_component->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
|
||||
}
|
||||
global_wifi_component->pending_.disconnect = true;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -568,8 +528,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
|
||||
if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) {
|
||||
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
|
||||
// we can't call retry_connect() from this context, so disconnect immediately
|
||||
// and notify main thread with error_from_callback_
|
||||
wifi_station_disconnect();
|
||||
global_wifi_component->error_from_callback_ = true;
|
||||
}
|
||||
@@ -583,10 +541,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf));
|
||||
s_sta_got_ip = true;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : global_wifi_component->ip_state_listeners_) {
|
||||
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0),
|
||||
global_wifi_component->get_dns_address(1));
|
||||
}
|
||||
// Defer listener callbacks to main loop - system context has limited stack
|
||||
global_wifi_component->pending_.got_ip = true;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -679,21 +635,15 @@ void WiFiComponent::wifi_pre_setup_() {
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
station_status_t status = wifi_station_get_connect_status();
|
||||
switch (status) {
|
||||
case STATION_GOT_IP:
|
||||
return WiFiSTAConnectStatus::CONNECTED;
|
||||
case STATION_NO_AP_FOUND:
|
||||
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
|
||||
;
|
||||
case STATION_CONNECT_FAIL:
|
||||
case STATION_WRONG_PASSWORD:
|
||||
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
||||
case STATION_CONNECTING:
|
||||
return WiFiSTAConnectStatus::CONNECTING;
|
||||
case STATION_IDLE:
|
||||
default:
|
||||
return WiFiSTAConnectStatus::IDLE;
|
||||
}
|
||||
if (status == STATION_GOT_IP)
|
||||
return WiFiSTAConnectStatus::CONNECTED;
|
||||
if (status == STATION_NO_AP_FOUND)
|
||||
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
|
||||
if (status == STATION_CONNECT_FAIL || status == STATION_WRONG_PASSWORD)
|
||||
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
||||
if (status == STATION_CONNECTING)
|
||||
return WiFiSTAConnectStatus::CONNECTING;
|
||||
return WiFiSTAConnectStatus::IDLE;
|
||||
}
|
||||
bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
static bool first_scan = false;
|
||||
@@ -798,9 +748,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
needs_full ? "" : " (filtered)");
|
||||
this->scan_done_ = true;
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : global_wifi_component->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(global_wifi_component->scan_result_);
|
||||
}
|
||||
this->pending_.scan_complete = true; // Defer listener callbacks to main loop
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -987,7 +935,34 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() {
|
||||
return network::IPAddress(&ip.gw);
|
||||
}
|
||||
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); }
|
||||
void WiFiComponent::wifi_loop_() {}
|
||||
void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); }
|
||||
|
||||
void WiFiComponent::process_pending_callbacks_() {
|
||||
// Process callbacks deferred from ESP8266 SDK system context (~2KB stack)
|
||||
// to main loop context (full stack). Connect state listeners are handled
|
||||
// by notify_connect_state_listeners_() in the shared state machine code.
|
||||
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
if (this->pending_.disconnect) {
|
||||
this->pending_.disconnect = false;
|
||||
this->notify_disconnect_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
if (this->pending_.got_ip) {
|
||||
this->pending_.got_ip = false;
|
||||
this->notify_ip_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
if (this->pending_.scan_complete) {
|
||||
this->pending_.scan_complete = false;
|
||||
this->notify_scan_results_listeners_();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::wifi
|
||||
#endif
|
||||
|
||||
@@ -753,9 +753,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
this->notify_ip_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -779,10 +777,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
s_sta_connecting = false;
|
||||
error_from_callback_ = true;
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
|
||||
}
|
||||
this->notify_disconnect_state_listeners_();
|
||||
#endif
|
||||
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) {
|
||||
@@ -793,9 +788,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
|
||||
this->got_ipv4_address_ = true;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
this->notify_ip_state_listeners_();
|
||||
#endif
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
@@ -804,9 +797,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip));
|
||||
this->num_ipv6_addresses_++;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
this->notify_ip_state_listeners_();
|
||||
#endif
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
@@ -883,9 +874,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
}
|
||||
this->notify_scan_results_listeners_();
|
||||
#endif
|
||||
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) {
|
||||
|
||||
@@ -468,9 +468,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
s_sta_state = LTWiFiSTAState::CONNECTED;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
this->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -527,10 +525,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
|
||||
}
|
||||
this->notify_disconnect_state_listeners_();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -553,18 +548,14 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf));
|
||||
s_sta_state = LTWiFiSTAState::CONNECTED;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
this->notify_ip_state_listeners_();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
|
||||
ESP_LOGV(TAG, "Got IPv6");
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
this->notify_ip_state_listeners_();
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -708,9 +699,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
needs_full ? "" : " (filtered)");
|
||||
WiFi.scanDelete();
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
}
|
||||
this->notify_scan_results_listeners_();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -264,9 +264,7 @@ void WiFiComponent::wifi_loop_() {
|
||||
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
}
|
||||
this->notify_scan_results_listeners_();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -290,9 +288,7 @@ void WiFiComponent::wifi_loop_() {
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
s_sta_had_ip = true;
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
this->notify_ip_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
} else if (!is_connected && s_sta_was_connected) {
|
||||
@@ -301,10 +297,7 @@ void WiFiComponent::wifi_loop_() {
|
||||
s_sta_had_ip = false;
|
||||
ESP_LOGV(TAG, "Disconnected");
|
||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||
static constexpr uint8_t EMPTY_BSSID[6] = {};
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID);
|
||||
}
|
||||
this->notify_disconnect_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -322,9 +315,7 @@ void WiFiComponent::wifi_loop_() {
|
||||
s_sta_had_ip = true;
|
||||
ESP_LOGV(TAG, "Got IP address");
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
this->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1403,6 +1403,17 @@ def requires_component(comp):
|
||||
return validator
|
||||
|
||||
|
||||
def conflicts_with_component(comp):
|
||||
"""Validate that this option cannot be specified when the component `comp` is loaded."""
|
||||
|
||||
def validator(value):
|
||||
if comp in CORE.loaded_integrations:
|
||||
raise Invalid(f"This option is not compatible with component {comp}")
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
uint8_t = int_range(min=0, max=255)
|
||||
uint16_t = int_range(min=0, max=65535)
|
||||
uint32_t = int_range(min=0, max=4294967295)
|
||||
|
||||
@@ -295,7 +295,7 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
|
||||
size_t chars = std::min(length, 2 * count);
|
||||
for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) {
|
||||
uint8_t val = parse_hex_char(*str);
|
||||
if (val > 15)
|
||||
if (val == INVALID_HEX_CHAR)
|
||||
return 0;
|
||||
data[i >> 1] = (i & 1) ? data[i >> 1] | val : val << 4;
|
||||
}
|
||||
|
||||
@@ -874,6 +874,9 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> optional<
|
||||
}
|
||||
|
||||
/// Parse a hex character to its nibble value (0-15), returns 255 on invalid input
|
||||
/// Returned by parse_hex_char() for non-hex characters.
|
||||
static constexpr uint8_t INVALID_HEX_CHAR = 255;
|
||||
|
||||
constexpr uint8_t parse_hex_char(char c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
@@ -881,7 +884,7 @@ constexpr uint8_t parse_hex_char(char c) {
|
||||
return c - 'A' + 10;
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
return 255;
|
||||
return INVALID_HEX_CHAR;
|
||||
}
|
||||
|
||||
/// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/hal.h" // For PROGMEM definition
|
||||
|
||||
// Platform-agnostic macros for PROGMEM string handling
|
||||
// On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings
|
||||
// On other platforms: Use plain strings (no PROGMEM)
|
||||
@@ -32,3 +38,80 @@ using ProgmemStr = const __FlashStringHelper *;
|
||||
// Type for pointers to strings (no PROGMEM on non-ESP8266 platforms)
|
||||
using ProgmemStr = const char *;
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Helper for C++20 string literal template arguments
|
||||
template<size_t N> struct FixedString {
|
||||
char data[N]{};
|
||||
constexpr FixedString(const char (&str)[N]) {
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
data[i] = str[i];
|
||||
}
|
||||
constexpr size_t size() const { return N - 1; } // exclude null terminator
|
||||
};
|
||||
|
||||
/// Compile-time string table that packs strings into a single blob with offset lookup.
|
||||
/// Use PROGMEM_STRING_TABLE macro to instantiate with proper flash placement on ESP8266.
|
||||
///
|
||||
/// Example:
|
||||
/// PROGMEM_STRING_TABLE(MyStrings, "foo", "bar", "baz");
|
||||
/// ProgmemStr str = MyStrings::get_progmem_str(idx, MyStrings::LAST_INDEX); // For ArduinoJson
|
||||
/// const LogString *log_str = MyStrings::get_log_str(idx, MyStrings::LAST_INDEX); // For logging
|
||||
///
|
||||
template<FixedString... Strs> struct ProgmemStringTable {
|
||||
static constexpr size_t COUNT = sizeof...(Strs);
|
||||
static constexpr size_t BLOB_SIZE = (0 + ... + (Strs.size() + 1));
|
||||
|
||||
/// Generate packed string blob at compile time
|
||||
static constexpr auto make_blob() {
|
||||
std::array<char, BLOB_SIZE> result{};
|
||||
size_t pos = 0;
|
||||
auto copy = [&](const auto &str) {
|
||||
for (size_t i = 0; i <= str.size(); ++i)
|
||||
result[pos++] = str.data[i];
|
||||
};
|
||||
(copy(Strs), ...);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate offset table at compile time (uint8_t limits blob to 255 bytes)
|
||||
static constexpr auto make_offsets() {
|
||||
static_assert(COUNT > 0, "PROGMEM_STRING_TABLE must contain at least one string");
|
||||
static_assert(COUNT <= 255, "PROGMEM_STRING_TABLE supports at most 255 strings with uint8_t indices");
|
||||
static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings");
|
||||
std::array<uint8_t, COUNT> result{};
|
||||
size_t pos = 0, idx = 0;
|
||||
((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declaration for LogString (defined in log.h)
|
||||
struct LogString;
|
||||
|
||||
/// Instantiate a ProgmemStringTable with PROGMEM storage.
|
||||
/// Creates: Name::get_progmem_str(idx, fallback), Name::get_log_str(idx, fallback)
|
||||
/// If idx >= COUNT, returns string at fallback. Use LAST_INDEX for common patterns.
|
||||
#define PROGMEM_STRING_TABLE(Name, ...) \
|
||||
struct Name { \
|
||||
using Table = ::esphome::ProgmemStringTable<__VA_ARGS__>; \
|
||||
static constexpr size_t COUNT = Table::COUNT; \
|
||||
static constexpr uint8_t LAST_INDEX = COUNT - 1; \
|
||||
static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \
|
||||
static constexpr auto BLOB PROGMEM = Table::make_blob(); \
|
||||
static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \
|
||||
static const char *get_(uint8_t idx, uint8_t fallback) { \
|
||||
if (idx >= COUNT) \
|
||||
idx = fallback; \
|
||||
return &BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]; \
|
||||
} \
|
||||
static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \
|
||||
return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \
|
||||
} \
|
||||
static const ::esphome::LogString *get_log_str(uint8_t idx, uint8_t fallback) { \
|
||||
return reinterpret_cast<const ::esphome::LogString *>(get_(idx, fallback)); \
|
||||
} \
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -37,7 +37,7 @@ lib_deps_base =
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||
esphome/dsmr_parser@1.0.0 ; dsmr
|
||||
esphome/dsmr_parser@1.1.0 ; dsmr
|
||||
polargoose/Crypto-no-arduino@0.4.0 ; dsmr
|
||||
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
|
||||
; This is using the repository until a new release is published to PlatformIO
|
||||
|
||||
@@ -23,6 +23,7 @@ resvg-py==0.2.6
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.1
|
||||
requests==2.32.5
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.0
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""
|
||||
Test schema.extend functionality in esphome.config_validation.
|
||||
Test config_validation functionality in esphome.config_validation.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_config_extend() -> None:
|
||||
@@ -49,3 +53,37 @@ def test_config_extend() -> None:
|
||||
assert validated["key2"] == "initial_value2"
|
||||
assert validated["extra_1"] == "value1"
|
||||
assert validated["extra_2"] == "value2"
|
||||
|
||||
|
||||
def test_requires_component_passes_when_loaded() -> None:
|
||||
"""Test requires_component passes when the required component is loaded."""
|
||||
CORE.loaded_integrations.update({"wifi", "logger"})
|
||||
validator = cv.requires_component("wifi")
|
||||
result = validator("test_value")
|
||||
assert result == "test_value"
|
||||
|
||||
|
||||
def test_requires_component_fails_when_not_loaded() -> None:
|
||||
"""Test requires_component raises Invalid when the required component is not loaded."""
|
||||
CORE.loaded_integrations.add("logger")
|
||||
validator = cv.requires_component("wifi")
|
||||
with pytest.raises(Invalid) as exc_info:
|
||||
validator("test_value")
|
||||
assert "requires component wifi" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_conflicts_with_component_passes_when_not_loaded() -> None:
|
||||
"""Test conflicts_with_component passes when the conflicting component is not loaded."""
|
||||
CORE.loaded_integrations.update({"wifi", "logger"})
|
||||
validator = cv.conflicts_with_component("esp32_hosted")
|
||||
result = validator("test_value")
|
||||
assert result == "test_value"
|
||||
|
||||
|
||||
def test_conflicts_with_component_fails_when_loaded() -> None:
|
||||
"""Test conflicts_with_component raises Invalid when the conflicting component is loaded."""
|
||||
CORE.loaded_integrations.update({"wifi", "esp32_hosted"})
|
||||
validator = cv.conflicts_with_component("esp32_hosted")
|
||||
with pytest.raises(Invalid) as exc_info:
|
||||
validator("test_value")
|
||||
assert "not compatible with component esp32_hosted" in str(exc_info.value)
|
||||
|
||||
8
tests/components/esp32_ble/test.esp32-p4-idf.yaml
Normal file
8
tests/components/esp32_ble/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
esp32_ble:
|
||||
io_capability: keyboard_only
|
||||
disable_bt_logs: false
|
||||
7
tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml
Normal file
7
tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml
|
||||
|
||||
# tx_power is not supported on ESP-Hosted platforms
|
||||
esp32_ble_beacon:
|
||||
type: iBeacon
|
||||
uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98'
|
||||
6
tests/components/esp32_ble_client/test.esp32-p4-idf.yaml
Normal file
6
tests/components/esp32_ble_client/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml
|
||||
|
||||
ble_client:
|
||||
- mac_address: 01:02:03:04:05:06
|
||||
id: blec
|
||||
4
tests/components/esp32_ble_server/test.esp32-p4-idf.yaml
Normal file
4
tests/components/esp32_ble_server/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
esp32_ble_tracker:
|
||||
max_connections: 9
|
||||
21
tests/test_build_components/common/ble/esp32-p4-idf.yaml
Normal file
21
tests/test_build_components/common/ble/esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# Common BLE tracker configuration for ESP32-P4 IDF tests
|
||||
# ESP32-P4 requires ESP-Hosted for Bluetooth via external coprocessor
|
||||
# BLE client components share this tracker infrastructure
|
||||
# Each component defines its own ble_client with unique MAC address
|
||||
|
||||
esp32_hosted:
|
||||
active_high: true
|
||||
variant: ESP32C6
|
||||
reset_pin: GPIO54
|
||||
cmd_pin: GPIO19
|
||||
clk_pin: GPIO18
|
||||
d0_pin: GPIO14
|
||||
d1_pin: GPIO15
|
||||
d2_pin: GPIO16
|
||||
d3_pin: GPIO17
|
||||
|
||||
esp32_ble_tracker:
|
||||
scan_parameters:
|
||||
interval: 1100ms
|
||||
window: 1100ms
|
||||
active: true
|
||||
Reference in New Issue
Block a user