diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 87b4ebb2c6..c32978d411 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a +9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ead87ad087..965e23870d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537 # v4 + uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4 with: run: ${{ steps.build.outputs.binary }} mode: simulation diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2ef1a5af31..6baab70b42 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: category: "/language:${{matrix.language}}" diff --git a/CODEOWNERS b/CODEOWNERS index 88f62c3194..e3e09cbc11 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -457,6 +457,8 @@ esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt +esphome/components/spa06_base/* @danielkent-net +esphome/components/spa06_i2c/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 7954c22822..f56d720ec2 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -56,6 +56,10 @@ _COMPONENT_PREFIX_LIB = "[lib]" _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" +# Placement new storage suffix (generated by codegen Pvariable) +_PSTORAGE_SUFFIX = "__pstorage" + + # C++ namespace prefixes _NAMESPACE_ESPHOME = "esphome::" _NAMESPACE_STD = "std::" @@ -201,6 +205,9 @@ class MemoryAnalyzer: self._cswtch_symbols: list[tuple[str, int, str, str]] = [] # Library symbol mapping: symbol_name -> library_name self._lib_symbol_map: dict[str, str] = {} + # Source file symbol mapping: symbol_name -> component_name + # Used for extern "C" and other symbols without C++ namespace + self._source_symbol_map: dict[str, str] = {} # Library dir to name mapping: "lib641" -> "espsoftwareserial", # "espressif__mdns" -> "mdns" self._lib_hash_to_name: dict[str, str] = {} @@ -214,6 +221,7 @@ class MemoryAnalyzer: self._parse_sections() self._parse_symbols() self._scan_libraries() + self._scan_source_symbols() self._categorize_symbols() self._analyze_cswtch_symbols() self._analyze_sdk_libraries() @@ -328,6 +336,13 @@ class MemoryAnalyzer: # Demangle C++ names if needed demangled = self._demangle_symbol(symbol_name) + # Check for placement new storage symbols (generated by codegen) + # Format: {component}__{id}__pstorage + if demangled.endswith(_PSTORAGE_SUFFIX) and ( + component := self._match_pstorage_component(demangled) + ): + return component + # Check for special component classes first (before namespace pattern) # This handles cases like esphome::ESPHomeOTAComponent which should map to ota if _NAMESPACE_ESPHOME in demangled: @@ -363,6 +378,11 @@ class MemoryAnalyzer: if lib_name := self._lib_symbol_map.get(symbol_name): return f"{_COMPONENT_PREFIX_LIB}{lib_name}" + # Check source file mapping (catches extern "C" functions in ESPHome sources) + # Must be before heuristic patterns since source attribution is authoritative + if component := self._source_symbol_map.get(symbol_name): + return component + # Check against symbol patterns for component, patterns in SYMBOL_PATTERNS.items(): if any(pattern in symbol_name for pattern in patterns): @@ -390,6 +410,24 @@ class MemoryAnalyzer: # Track uncategorized symbols for analysis return "other" + def _match_pstorage_component(self, symbol_name: str) -> str | None: + """Match a __pstorage symbol to its ESPHome component. + + Symbol format: {component}__{id}__pstorage + The component namespace is embedded by codegen before the double underscore. + """ + prefix = symbol_name[: -len(_PSTORAGE_SUFFIX)] + # Extract component namespace before the first double underscore + dunder_pos = prefix.find("__") + if dunder_pos == -1: + return None + component_name = prefix[:dunder_pos] + 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}" + return None + def _batch_demangle_symbols(self, symbols: list[str]) -> None: """Batch demangle C++ symbol names for efficiency.""" if not symbols: @@ -653,6 +691,7 @@ class MemoryAnalyzer: return None symbol_map: dict[str, str] = {} + source_symbol_map: dict[str, str] = {} current_symbol: str | None = None section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.") @@ -688,9 +727,18 @@ class MemoryAnalyzer: if dir_key in source_path: symbol_map[current_symbol] = lib_name break + else: + # Map ESPHome source files to components for extern "C" + # and other symbols without C++ namespace + component = self._source_file_to_component(source_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_symbol_map[current_symbol] = component current_symbol = None + self._source_symbol_map = source_symbol_map return symbol_map or None def _scan_libraries(self) -> None: @@ -741,6 +789,112 @@ class MemoryAnalyzer: len(libraries), ) + def _scan_source_symbols(self) -> None: + """Scan ESPHome source object files to map extern "C" symbols to components. + + When no linker map file is available, this uses ``nm`` to scan ``.o`` files + under ``src/esphome/`` and build a symbol-to-component mapping. This catches + ``extern "C"`` functions and other symbols that lack C++ namespace prefixes. + + Skips scanning if ``_source_symbol_map`` was already populated by + ``_parse_map_file()``. + """ + if self._source_symbol_map or not self.nm_path: + return + + obj_dir = self._find_object_files_dir() + if obj_dir is None: + return + + # Find ESPHome source object files + esphome_src_dir = obj_dir / "src" / "esphome" + if not esphome_src_dir.is_dir(): + return + + obj_files = sorted(esphome_src_dir.rglob("*.o")) + if not obj_files: + return + + # Run nm with --print-file-name to get file:symbol mapping + result = run_tool( + [self.nm_path, "--print-file-name", "-g", "--defined-only"] + + [str(f) for f in obj_files], + ) + if result is None or result.returncode != 0: + _LOGGER.debug("nm scan of source objects failed") + return + + self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir) + if self._source_symbol_map: + _LOGGER.info( + "Built source symbol map from nm: %d symbols", + len(self._source_symbol_map), + ) + + def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]: + """Parse nm output to map non-namespaced symbols to ESPHome components. + + Extracts global defined symbols from ESPHome source object files that + don't use C++ namespacing (e.g. ``extern "C"`` functions). + + Args: + output: Raw stdout from ``nm --print-file-name -g --defined-only`` + or ``nm --print-file-name -S``. + base_dir: Build directory for computing relative paths. + + Returns: + Dict mapping symbol names to component names. + """ + source_map: dict[str, str] = {} + for line in output.splitlines(): + # Format: /path/to/file.o: addr type name + # or: /path/to/file.o: addr size type name (with -S) + colon_idx = line.rfind(".o:") + if colon_idx == -1: + continue + + file_path = line[: colon_idx + 2] + fields = line[colon_idx + 3 :].split() + if len(fields) < 3: + continue + + # With -S flag, format is: addr size type name + # Without -S flag: addr type name + # type is a single char; size is hex digits + # Detect by checking if fields[1] is a single uppercase letter (type) + if len(fields[1]) == 1 and fields[1].isalpha(): + # addr type name + sym_type = fields[1] + symbol_name = fields[2] + elif len(fields) >= 4: + # addr size type name + sym_type = fields[2] + symbol_name = fields[3] + else: + continue + + # Only global defined symbols (uppercase type) + if not sym_type.isupper() or sym_type == "U": + continue + + # Skip symbols already in esphome:: namespace + if symbol_name.startswith("_ZN7esphome"): + continue + + # Make path relative to base_dir for _source_file_to_component + try: + rel_path = str(Path(file_path).relative_to(base_dir)) + except ValueError: + continue + + component = self._source_file_to_component(rel_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_map[symbol_name] = component + + return source_map + def _find_object_files_dir(self) -> Path | None: """Find the directory containing object files for this build. diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index acaf5f4562..b7561e8ffc 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -15,6 +15,7 @@ from . import ( _COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL, _COMPONENT_PREFIX_LIB, + _PSTORAGE_SUFFIX, RAM_SECTIONS, MemoryAnalyzer, ) @@ -23,6 +24,17 @@ if TYPE_CHECKING: from . import ComponentMemory +def _format_pstorage_name(name: str) -> str: + """Format a __pstorage symbol as 'storage for {id}'.""" + if not name.endswith(_PSTORAGE_SUFFIX): + return name + prefix = name[: -len(_PSTORAGE_SUFFIX)] + # Strip component namespace prefix: {component}__{id} -> {id} + dunder_pos = prefix.find("__") + var_id = prefix[dunder_pos + 2 :] if dunder_pos != -1 else prefix + return f"storage for {var_id}" + + class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" @@ -148,11 +160,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): If section is one of the RAM sections (.data or .bss), a label like " [data]" or " [bss]" is appended. For non-RAM sections or when section is None, no section label is added. + + Placement new storage symbols are formatted as "storage for {id}". """ + display_name = _format_pstorage_name(demangled) section_label = "" if section in RAM_SECTIONS: section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] - return f"{demangled} ({size:,} B){section_label}" + return f"{display_name} ({size:,} B){section_label}" def _add_top_symbols(self, lines: list[str]) -> None: """Add a section showing the top largest symbols in the binary.""" @@ -175,11 +190,13 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for i, (_, demangled, size, section, component) in enumerate(top_symbols): # Format section label section_label = f"[{section[1:]}]" if section else "" - # Truncate demangled name if too long + # Format storage symbols readably + display_name = _format_pstorage_name(demangled) + # Truncate if too long demangled_display = ( - f"{demangled[:truncate_limit]}..." - if len(demangled) > self.COL_TOP_SYMBOL_NAME - else demangled + f"{display_name[:truncate_limit]}..." + if len(display_name) > self.COL_TOP_SYMBOL_NAME + else display_name ) lines.append( f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" @@ -573,15 +590,16 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols above threshold for better visibility + # Show symbols above threshold, always include storage symbols large_symbols = [ (sym, dem, size, sec) for sym, dem, size, sec in sorted_symbols if size > self.SYMBOL_SIZE_THRESHOLD + or dem.endswith(_PSTORAGE_SUFFIX) ] lines.append( - f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size, section) in enumerate(large_symbols): lines.append( @@ -604,7 +622,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): # Sort by size descending sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True) large_ram_syms = [ - s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + s + for s in sorted_ram_syms + if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + or s[1].endswith(_PSTORAGE_SUFFIX) ] lines.append(f"{name} ({mem.ram_total:,} B total RAM):") @@ -622,13 +643,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for symbol, demangled, size, section in large_ram_syms[:10]: # Format section label consistently by stripping leading dot section_label = section.lstrip(".") if section else "" + display_name = _format_pstorage_name(demangled) # Add ellipsis if name is truncated - demangled_display = ( - f"{demangled[:70]}..." if len(demangled) > 70 else demangled - ) - lines.append( - f" {size:>6,} B [{section_label}] {demangled_display}" + display_name = ( + f"{display_name[:70]}..." + if len(display_name) > 70 + else display_name ) + lines.append(f" {size:>6,} B [{section_label}] {display_name}") if len(large_ram_syms) > 10: lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append("") diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 3bdf555ae3..0c871d0727 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -408,7 +408,6 @@ SYMBOL_PATTERNS = { ], "arduino_core": [ "pinMode", - "resetPins", "millis", "micros", "delay(", # More specific - Arduino delay function with parenthesis diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 40676f8655..3137a59fad 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -1,22 +1,29 @@ #include "esphome/core/log.h" #include "absolute_humidity.h" -namespace esphome { -namespace absolute_humidity { +namespace esphome::absolute_humidity { -static const char *const TAG = "absolute_humidity.sensor"; +static const char *const TAG{"absolute_humidity.sensor"}; void AbsoluteHumidityComponent::setup() { + this->temperature_sensor_->add_on_state_callback([this](float state) { + this->temperature_ = state; + this->enable_loop(); + }); ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); - this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); + // Get initial value if (this->temperature_sensor_->has_state()) { - this->temperature_callback_(this->temperature_sensor_->get_state()); + this->temperature_ = this->temperature_sensor_->get_state(); } + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->humidity_ = state; + this->enable_loop(); + }); ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str()); - this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); }); + // Get initial value if (this->humidity_sensor_->has_state()) { - this->humidity_callback_(this->humidity_sensor_->get_state()); + this->humidity_ = this->humidity_sensor_->get_state(); } } @@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() { } void AbsoluteHumidityComponent::loop() { - if (!this->next_update_) { - return; - } - this->next_update_ = false; + // Only run once + this->disable_loop(); // Ensure we have source data - const bool no_temperature = std::isnan(this->temperature_); - const bool no_humidity = std::isnan(this->humidity_); + const bool no_temperature{std::isnan(this->temperature_)}; + const bool no_humidity{std::isnan(this->humidity_)}; if (no_temperature || no_humidity) { if (no_temperature) { ESP_LOGW(TAG, "No valid state from temperature sensor!"); @@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() { } // Convert to desired units - const float temperature_c = this->temperature_; - const float temperature_k = temperature_c + 273.15; - const float hr = this->humidity_ / 100; + const float temperature_c{this->temperature_}; + const float temperature_k{temperature_c + 273.15f}; + const float hr{this->humidity_ / 100.0f}; // Calculate saturation vapor pressure float es; @@ -90,7 +95,7 @@ void AbsoluteHumidityComponent::loop() { } // Calculate absolute humidity - const float absolute_humidity = vapor_density(es, hr, temperature_k); + const float absolute_humidity{vapor_density(es, hr, temperature_k)}; ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); @@ -103,16 +108,16 @@ void AbsoluteHumidityComponent::loop() { // More accurate than Tetens in normal meteorologic conditions float AbsoluteHumidityComponent::es_buck(float temperature_c) { float a, b, c, d; - if (temperature_c >= 0) { - a = 0.61121; - b = 18.678; - c = 234.5; - d = 257.14; + if (temperature_c >= 0.0f) { + a = 0.61121f; + b = 18.678f; + c = 234.5f; + d = 257.14f; } else { - a = 0.61115; - b = 18.678; - c = 233.7; - d = 279.82; + a = 0.61115f; + b = 18.678f; + c = 233.7f; + d = 279.82f; } return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c))); } @@ -120,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) { // Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation) float AbsoluteHumidityComponent::es_tetens(float temperature_c) { float a, b; - if (temperature_c >= 0) { - a = 17.27; - b = 237.3; + if (temperature_c >= 0.0f) { + a = 17.27f; + b = 237.3f; } else { - a = 21.875; - b = 265.5; + a = 21.875f; + b = 265.5f; } - return 0.61078 * expf((a * temperature_c) / (temperature_c + b)); + return 0.61078f * expf((a * temperature_c) / (temperature_c + b)); } // Wobus equation @@ -146,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) { // // Baker, Schlatter 17-MAY-1982 Original version. - const float c0 = +0.99999683e00; - const float c1 = -0.90826951e-02; - const float c2 = +0.78736169e-04; - const float c3 = -0.61117958e-06; - const float c4 = +0.43884187e-08; - const float c5 = -0.29883885e-10; - const float c6 = +0.21874425e-12; - const float c7 = -0.17892321e-14; - const float c8 = +0.11112018e-16; - const float c9 = -0.30994571e-19; - const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9))))))))); - return 0.61078 / pow(p, 8); + constexpr float c0{+0.99999683e+00f}; + constexpr float c1{-0.90826951e-02f}; + constexpr float c2{+0.78736169e-04f}; + constexpr float c3{-0.61117958e-06f}; + constexpr float c4{+0.43884187e-08f}; + constexpr float c5{-0.29883885e-10f}; + constexpr float c6{+0.21874425e-12f}; + constexpr float c7{-0.17892321e-14f}; + constexpr float c8{+0.11112018e-16f}; + constexpr float c9{-0.30994571e-19f}; + const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))}; + return 0.61078f / powf(p, 8.0f); } // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ @@ -168,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) { // hr = relative humidity [0-1] // ta = absolute temperature (K) - const float ea = hr * es * 1000; // vapor pressure of the air (Pa) - const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹) - const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹) + const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa) + const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹) + const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹) return (ea * mw) / (r * ta); } -} // namespace absolute_humidity -} // namespace esphome +} // namespace esphome::absolute_humidity diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h index 71feee2c42..be28d3dc50 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.h +++ b/esphome/components/absolute_humidity/absolute_humidity.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace absolute_humidity { +namespace esphome::absolute_humidity { /// Enum listing all implemented saturation vapor pressure equations. enum SaturationVaporPressureEquation { @@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation { /// This class implements calculation of absolute humidity from temperature and relative humidity. class AbsoluteHumidityComponent : public sensor::Sensor, public Component { public: - AbsoluteHumidityComponent() = default; - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; } @@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { void loop() override; protected: - void temperature_callback_(float state) { - this->next_update_ = true; - this->temperature_ = state; - } - void humidity_callback_(float state) { - this->next_update_ = true; - this->humidity_ = state; - } - /** Buck equation for saturation vapor pressure in kPa. * * @param temperature_c Air temperature in °C. @@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { * @param es Saturation vapor pressure in kPa. * @param hr Relative humidity 0 to 1. * @param ta Absolute temperature in K. - * @param heater_duration The duration in ms that the heater should turn on for when measuring. */ static float vapor_density(float es, float hr, float ta); sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; - bool next_update_{false}; - float temperature_{NAN}; float humidity_{NAN}; SaturationVaporPressureEquation equation_; }; -} // namespace absolute_humidity -} // namespace esphome +} // namespace esphome::absolute_humidity diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 9ea95d8570..dd70768105 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/sensor/sensor.h" diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 4c3cf81927..84589d540d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -455,6 +455,9 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") cg.add_library("esphome/noise-c", "0.1.11") + # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops + cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") + cg.add_build_flag("-DHAVE_INLINE_ASM=1") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 28332d67a5..86daa9a2bf 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -316,7 +316,7 @@ message ListEntitiesBinarySensorResponse { option (ifdef) = "USE_BINARY_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -334,7 +334,7 @@ message BinarySensorStateResponse { option (ifdef) = "USE_BINARY_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; // If the binary sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -350,7 +350,7 @@ message ListEntitiesCoverResponse { option (ifdef) = "USE_COVER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -383,7 +383,7 @@ message CoverStateResponse { option (ifdef) = "USE_COVER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // legacy: state has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change // Deprecated in API version 1.1 @@ -409,7 +409,7 @@ message CoverCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // legacy: command has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change @@ -434,7 +434,7 @@ message ListEntitiesFanResponse { option (ifdef) = "USE_FAN"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -466,7 +466,7 @@ message FanStateResponse { option (ifdef) = "USE_FAN"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; bool oscillating = 3; // Deprecated in API version 1.6 @@ -483,7 +483,7 @@ message FanCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; // Deprecated in API version 1.6 @@ -522,7 +522,7 @@ message ListEntitiesLightResponse { option (ifdef) = "USE_LIGHT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -551,7 +551,7 @@ message LightStateResponse { option (ifdef) = "USE_LIGHT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; float brightness = 3; ColorMode color_mode = 11; @@ -573,7 +573,7 @@ message LightCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; bool has_brightness = 4; @@ -627,7 +627,7 @@ message ListEntitiesSensorResponse { option (ifdef) = "USE_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -651,7 +651,7 @@ message SensorStateResponse { option (ifdef) = "USE_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; // If the sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -667,7 +667,7 @@ message ListEntitiesSwitchResponse { option (ifdef) = "USE_SWITCH"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -685,7 +685,7 @@ message SwitchStateResponse { option (ifdef) = "USE_SWITCH"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -696,7 +696,7 @@ message SwitchCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -709,7 +709,7 @@ message ListEntitiesTextSensorResponse { option (ifdef) = "USE_TEXT_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -726,7 +726,7 @@ message TextSensorStateResponse { option (ifdef) = "USE_TEXT_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the text sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -922,7 +922,7 @@ message ListEntitiesServicesResponse { option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; SupportsResponseType supports_response = 4; } @@ -945,7 +945,7 @@ message ExecuteServiceRequest { option (no_delay) = true; option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; @@ -972,7 +972,7 @@ message ListEntitiesCameraResponse { option (ifdef) = "USE_CAMERA"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; @@ -987,7 +987,7 @@ message CameraImageResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CAMERA"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bytes data = 2; bool done = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -1057,7 +1057,7 @@ message ListEntitiesClimateResponse { option (ifdef) = "USE_CLIMATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1095,7 +1095,7 @@ message ClimateStateResponse { option (ifdef) = "USE_CLIMATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; ClimateMode mode = 2; float current_temperature = 3; float target_temperature = 4; @@ -1121,7 +1121,7 @@ message ClimateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_mode = 2; ClimateMode mode = 3; bool has_target_temperature = 4; @@ -1168,7 +1168,7 @@ message ListEntitiesWaterHeaterResponse { option (ifdef) = "USE_WATER_HEATER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; @@ -1189,7 +1189,7 @@ message WaterHeaterStateResponse { option (ifdef) = "USE_WATER_HEATER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float current_temperature = 2; float target_temperature = 3; WaterHeaterMode mode = 4; @@ -1219,7 +1219,7 @@ message WaterHeaterCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // Bitmask of which fields are set (see WaterHeaterCommandHasField) uint32 has_fields = 2; WaterHeaterMode mode = 3; @@ -1244,7 +1244,7 @@ message ListEntitiesNumberResponse { option (ifdef) = "USE_NUMBER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1266,7 +1266,7 @@ message NumberStateResponse { option (ifdef) = "USE_NUMBER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; // If the number does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -1280,7 +1280,7 @@ message NumberCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1293,7 +1293,7 @@ message ListEntitiesSelectResponse { option (ifdef) = "USE_SELECT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1310,7 +1310,7 @@ message SelectStateResponse { option (ifdef) = "USE_SELECT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the select does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -1324,7 +1324,7 @@ message SelectCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1337,7 +1337,7 @@ message ListEntitiesSirenResponse { option (ifdef) = "USE_SIREN"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1356,7 +1356,7 @@ message SirenStateResponse { option (ifdef) = "USE_SIREN"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1367,7 +1367,7 @@ message SirenCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; bool has_tone = 4; @@ -1400,7 +1400,7 @@ message ListEntitiesLockResponse { option (ifdef) = "USE_LOCK"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1422,7 +1422,7 @@ message LockStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; LockState state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1432,7 +1432,7 @@ message LockCommandRequest { option (ifdef) = "USE_LOCK"; option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; LockCommand command = 2; // Not yet implemented: @@ -1449,7 +1449,7 @@ message ListEntitiesButtonResponse { option (ifdef) = "USE_BUTTON"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1466,7 +1466,7 @@ message ButtonCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"]; } @@ -1516,7 +1516,7 @@ message ListEntitiesMediaPlayerResponse { option (ifdef) = "USE_MEDIA_PLAYER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1538,7 +1538,7 @@ message MediaPlayerStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; MediaPlayerState state = 2; float volume = 3; bool muted = 4; @@ -1551,7 +1551,7 @@ message MediaPlayerCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_command = 2; MediaPlayerCommand command = 3; @@ -2104,7 +2104,7 @@ message ListEntitiesAlarmControlPanelResponse { option (ifdef) = "USE_ALARM_CONTROL_PANEL"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2122,7 +2122,7 @@ message AlarmControlPanelStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; AlarmControlPanelState state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2133,7 +2133,7 @@ message AlarmControlPanelCommandRequest { option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; AlarmControlPanelStateCommand command = 2; string code = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -2151,7 +2151,7 @@ message ListEntitiesTextResponse { option (ifdef) = "USE_TEXT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2171,7 +2171,7 @@ message TextStateResponse { option (ifdef) = "USE_TEXT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the Text does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -2185,7 +2185,7 @@ message TextCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2199,7 +2199,7 @@ message ListEntitiesDateResponse { option (ifdef) = "USE_DATETIME_DATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2215,7 +2215,7 @@ message DateStateResponse { option (ifdef) = "USE_DATETIME_DATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the date does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2231,7 +2231,7 @@ message DateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 year = 2; uint32 month = 3; uint32 day = 4; @@ -2246,7 +2246,7 @@ message ListEntitiesTimeResponse { option (ifdef) = "USE_DATETIME_TIME"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2262,7 +2262,7 @@ message TimeStateResponse { option (ifdef) = "USE_DATETIME_TIME"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the time does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2278,7 +2278,7 @@ message TimeCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 hour = 2; uint32 minute = 3; uint32 second = 4; @@ -2293,7 +2293,7 @@ message ListEntitiesEventResponse { option (ifdef) = "USE_EVENT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2311,7 +2311,7 @@ message EventResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string event_type = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2324,7 +2324,7 @@ message ListEntitiesValveResponse { option (ifdef) = "USE_VALVE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2351,7 +2351,7 @@ message ValveStateResponse { option (ifdef) = "USE_VALVE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float position = 2; ValveOperation current_operation = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -2364,7 +2364,7 @@ message ValveCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_position = 2; float position = 3; bool stop = 4; @@ -2379,7 +2379,7 @@ message ListEntitiesDateTimeResponse { option (ifdef) = "USE_DATETIME_DATETIME"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2395,7 +2395,7 @@ message DateTimeStateResponse { option (ifdef) = "USE_DATETIME_DATETIME"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the datetime does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2409,7 +2409,7 @@ message DateTimeCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; fixed32 epoch_seconds = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2422,7 +2422,7 @@ message ListEntitiesUpdateResponse { option (ifdef) = "USE_UPDATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2439,7 +2439,7 @@ message UpdateStateResponse { option (ifdef) = "USE_UPDATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool missing_state = 2; bool in_progress = 3; bool has_progress = 4; @@ -2463,7 +2463,7 @@ message UpdateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; UpdateCommand command = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2505,7 +2505,7 @@ message ListEntitiesInfraredResponse { option (ifdef) = "USE_INFRARED"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; @@ -2521,7 +2521,7 @@ message InfraredRFTransmitRawTimingsRequest { option (ifdef) = "USE_IR_RF"; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2; // Key identifying the transmitter instance + fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance uint32 carrier_frequency = 3; // Carrier frequency in Hz uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.) repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off) @@ -2535,7 +2535,7 @@ message InfraredRFReceiveEvent { option (no_delay) = true; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2; // Key identifying the receiver instance + fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d55b5dffb6..d023cd21a8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -64,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * // A stalled handshake from a buggy client or network glitch holds a connection // slot, which can prevent legitimate clients from reconnecting. Also hardens // against the less likely case of intentional connection slot exhaustion. -static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000; +// +// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak +// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to +// 28-30s. See https://github.com/esphome/esphome/issues/14999 +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); @@ -230,7 +234,7 @@ void APIConnection::loop() { this->last_traffic_ = now; } // read a packet - this->read_message(buffer.data_len, buffer.type, buffer.data); + this->read_message_(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) return; } @@ -1515,16 +1519,16 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { resp.instance = msg.instance; resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; switch (proxies[msg.instance]->flush_port()) { - case uart::FlushResult::SUCCESS: + case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS: resp.status = enums::SERIAL_PROXY_STATUS_OK; break; - case uart::FlushResult::ASSUMED_SUCCESS: + case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS: resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; break; - case uart::FlushResult::TIMEOUT: + case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT: resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; break; - case uart::FlushResult::FAILED: + case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED: resp.status = enums::SERIAL_PROXY_STATUS_ERROR; break; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 85c8e777a9..3d8563b1ae 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -49,11 +49,29 @@ class APIConnection final : public APIServerConnectionBase { friend class APIServer; friend class ListEntitiesIterator; APIConnection(std::unique_ptr socket, APIServer *parent); - virtual ~APIConnection(); + ~APIConnection(); void start(); void loop(); + protected: + // read_message_ is defined here (instead of in APIServerConnectionBase) so the + // compiler can devirtualize and inline on_* handler calls within this final class. + void read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data); + + // Auth helpers defined here (not in ProtoService) so the compiler can + // devirtualize is_connection_setup()/on_no_setup_connection() calls + // within this final class. + inline bool check_connection_setup_() { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return false; + } + return true; + } + inline bool check_authenticated_() { return this->check_connection_setup_(); } + + public: bool send_list_info_done() { return this->schedule_message_(nullptr, ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE); @@ -63,72 +81,72 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void on_cover_command_request(const CoverCommandRequest &msg) override; + void on_cover_command_request(const CoverCommandRequest &msg); #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void on_fan_command_request(const FanCommandRequest &msg) override; + void on_fan_command_request(const FanCommandRequest &msg); #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void on_light_command_request(const LightCommandRequest &msg) override; + void on_light_command_request(const LightCommandRequest &msg); #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void on_switch_command_request(const SwitchCommandRequest &msg) override; + void on_switch_command_request(const SwitchCommandRequest &msg); #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); #endif #ifdef USE_CAMERA void set_camera_state(std::shared_ptr image); - void on_camera_image_request(const CameraImageRequest &msg) override; + void on_camera_image_request(const CameraImageRequest &msg); #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void on_climate_command_request(const ClimateCommandRequest &msg) override; + void on_climate_command_request(const ClimateCommandRequest &msg); #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void on_number_command_request(const NumberCommandRequest &msg) override; + void on_number_command_request(const NumberCommandRequest &msg); #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void on_date_command_request(const DateCommandRequest &msg) override; + void on_date_command_request(const DateCommandRequest &msg); #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void on_time_command_request(const TimeCommandRequest &msg) override; + void on_time_command_request(const TimeCommandRequest &msg); #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void on_date_time_command_request(const DateTimeCommandRequest &msg) override; + void on_date_time_command_request(const DateTimeCommandRequest &msg); #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void on_text_command_request(const TextCommandRequest &msg) override; + void on_text_command_request(const TextCommandRequest &msg); #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void on_select_command_request(const SelectCommandRequest &msg) override; + void on_select_command_request(const SelectCommandRequest &msg); #endif #ifdef USE_BUTTON - void on_button_command_request(const ButtonCommandRequest &msg) override; + void on_button_command_request(const ButtonCommandRequest &msg); #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void on_lock_command_request(const LockCommandRequest &msg) override; + void on_lock_command_request(const LockCommandRequest &msg); #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void on_valve_command_request(const ValveCommandRequest &msg) override; + void on_valve_command_request(const ValveCommandRequest &msg); #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; + void on_media_player_command_request(const MediaPlayerCommandRequest &msg); #endif bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); #ifdef USE_API_HOMEASSISTANT_SERVICES @@ -138,23 +156,23 @@ class APIConnection final : public APIServerConnectionBase { this->send_message(call); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; + void on_homeassistant_action_response(const HomeassistantActionResponse &msg); #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; - void on_unsubscribe_bluetooth_le_advertisements_request() override; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg); + void on_unsubscribe_bluetooth_le_advertisements_request(); - void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override; - void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override; - void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override; - void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override; - void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override; - void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override; - void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; - void on_subscribe_bluetooth_connections_free_request() override; - void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; - void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override; + void on_bluetooth_device_request(const BluetoothDeviceRequest &msg); + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg); + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg); + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg); + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg); + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg); + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg); + void on_subscribe_bluetooth_connections_free_request(); + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg); + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg); #endif #ifdef USE_HOMEASSISTANT_TIME @@ -165,42 +183,42 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_VOICE_ASSISTANT - void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; - void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; - void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; - void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; - void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; - void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; - void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override; - void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg); + void on_voice_assistant_response(const VoiceAssistantResponse &msg); + void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg); + void on_voice_assistant_audio(const VoiceAssistantAudio &msg); + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg); + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg); + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg); + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg); #endif #ifdef USE_ZWAVE_PROXY - void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override; - void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg); + void on_z_wave_proxy_request(const ZWaveProxyRequest &msg); #endif #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg); #endif #ifdef USE_WATER_HEATER bool send_water_heater_state(water_heater::WaterHeater *water_heater); - void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; + void on_water_heater_command_request(const WaterHeaterCommandRequest &msg); #endif #ifdef USE_IR_RF - void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg); void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif #ifdef USE_SERIAL_PROXY - void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override; - void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override; - void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override; - void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override; - void on_serial_proxy_request(const SerialProxyRequest &msg) override; + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg); + void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg); + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg); + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg); + void on_serial_proxy_request(const SerialProxyRequest &msg); void send_serial_proxy_data(const SerialProxyDataReceived &msg); #endif @@ -210,26 +228,26 @@ class APIConnection final : public APIServerConnectionBase { #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void on_update_command_request(const UpdateCommandRequest &msg) override; + void on_update_command_request(const UpdateCommandRequest &msg); #endif - void on_disconnect_response() override; - void on_ping_response() override { + void on_disconnect_response(); + void on_ping_response() { // we initiated ping this->flags_.sent_ping = false; } #ifdef USE_API_HOMEASSISTANT_STATES - void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; + void on_home_assistant_state_response(const HomeAssistantStateResponse &msg); #endif #ifdef USE_HOMEASSISTANT_TIME - void on_get_time_response(const GetTimeResponse &value) override; + void on_get_time_response(const GetTimeResponse &value); #endif - void on_hello_request(const HelloRequest &msg) override; - void on_disconnect_request() override; - void on_ping_request() override; - void on_device_info_request() override; - void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } - void on_subscribe_states_request() override { + void on_hello_request(const HelloRequest &msg); + void on_disconnect_request(); + void on_ping_request(); + void on_device_info_request(); + void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } + void on_subscribe_states_request() { this->flags_.state_subscription = true; // Start initial state iterator only if no iterator is active // If list_entities is running, we'll start initial_state when it completes @@ -237,7 +255,7 @@ class APIConnection final : public APIServerConnectionBase { this->begin_iterator_(ActiveIterator::INITIAL_STATE); } } - void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override { + void on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); @@ -249,13 +267,13 @@ class APIConnection final : public APIServerConnectionBase { #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES - void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; } + void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES - void on_subscribe_home_assistant_states_request() override; + void on_subscribe_home_assistant_states_request(); #endif #ifdef USE_API_USER_DEFINED_ACTIONS - void on_execute_service_request(const ExecuteServiceRequest &msg) override; + void on_execute_service_request(const ExecuteServiceRequest &msg); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON @@ -265,13 +283,13 @@ class APIConnection final : public APIServerConnectionBase { #endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_API_NOISE - void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg); #endif - bool is_authenticated() override { + bool is_authenticated() { return static_cast(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; } - bool is_connection_setup() override { + bool is_connection_setup() { return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } @@ -284,8 +302,8 @@ class APIConnection final : public APIServerConnectionBase { (this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor); } - void on_fatal_error() override; - void on_no_setup_connection() override; + void on_fatal_error(); + void on_no_setup_connection(); // Function pointer type for type-erased message encoding using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); @@ -324,7 +342,7 @@ class APIConnection final : public APIServerConnectionBase { return true; return this->try_to_clear_buffer_slow_(log_out_of_space); } - bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; + bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type); const char *get_name() const { return this->helper_->get_client_name(); } /// Get peer name (IP address) into caller-provided buffer, returns buf for convenience diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 01993cc5e5..61b034c7ea 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -208,7 +208,7 @@ uint32_t DeviceInfoResponse::calculate_size() const { #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); @@ -224,7 +224,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_length(1, this->device_class.size()); size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); @@ -239,7 +239,7 @@ uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { return size; } void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -248,7 +248,7 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t BinarySensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -260,7 +260,7 @@ uint32_t BinarySensorStateResponse::calculate_size() const { #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); @@ -279,7 +279,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesCoverResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); @@ -297,7 +297,7 @@ uint32_t ListEntitiesCoverResponse::calculate_size() const { return size; } void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); buffer.encode_uint32(5, static_cast(this->current_operation)); @@ -307,7 +307,7 @@ void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t CoverStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->position); size += ProtoSize::calc_float(1, this->tilt); size += ProtoSize::calc_uint32(1, static_cast(this->current_operation)); @@ -357,7 +357,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); @@ -378,7 +378,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesFanResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->supports_oscillation); size += ProtoSize::calc_bool(1, this->supports_speed); @@ -400,7 +400,7 @@ uint32_t ListEntitiesFanResponse::calculate_size() const { return size; } void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_uint32(5, static_cast(this->direction)); @@ -412,7 +412,7 @@ void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t FanStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_bool(1, this->oscillating); size += ProtoSize::calc_uint32(1, static_cast(this->direction)); @@ -487,7 +487,7 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); @@ -509,7 +509,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesLightResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { @@ -534,7 +534,7 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { return size; } void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); buffer.encode_uint32(11, static_cast(this->color_mode)); @@ -553,7 +553,7 @@ void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t LightStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); size += ProtoSize::calc_float(1, this->brightness); size += ProtoSize::calc_uint32(1, static_cast(this->color_mode)); @@ -683,7 +683,7 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -702,7 +702,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -720,7 +720,7 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const { return size; } void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -729,7 +729,7 @@ void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -741,7 +741,7 @@ uint32_t SensorStateResponse::calculate_size() const { #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -757,7 +757,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSwitchResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -772,7 +772,7 @@ uint32_t ListEntitiesSwitchResponse::calculate_size() const { return size; } void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -780,7 +780,7 @@ void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SwitchStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -816,7 +816,7 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -831,7 +831,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTextSensorResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -845,7 +845,7 @@ uint32_t ListEntitiesTextSensorResponse::calculate_size() const { return size; } void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -854,7 +854,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TextSensorStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1124,7 +1124,7 @@ uint32_t ListEntitiesServicesArgument::calculate_size() const { } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); for (auto &it : this->args) { buffer.encode_sub_message(3, it); } @@ -1133,7 +1133,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesServicesResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; if (!this->args.empty()) { for (const auto &it : this->args) { size += ProtoSize::calc_message_force(1, it.calculate_size()); @@ -1269,7 +1269,7 @@ uint32_t ExecuteServiceResponse::calculate_size() const { #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1283,7 +1283,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesCameraResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1296,7 +1296,7 @@ uint32_t ListEntitiesCameraResponse::calculate_size() const { return size; } void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); #ifdef USE_DEVICES @@ -1305,7 +1305,7 @@ void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t CameraImageResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->data_len_); size += ProtoSize::calc_bool(1, this->done); #ifdef USE_DEVICES @@ -1330,7 +1330,7 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); @@ -1374,7 +1374,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesClimateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); size += ProtoSize::calc_bool(1, this->supports_current_temperature); size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); @@ -1429,7 +1429,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { return size; } void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_float(3, this->current_temperature); buffer.encode_float(4, this->target_temperature); @@ -1449,7 +1449,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ClimateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast(this->mode)); size += ProtoSize::calc_float(1, this->current_temperature); size += ProtoSize::calc_float(1, this->target_temperature); @@ -1563,7 +1563,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_WATER_HEATER void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); @@ -1584,7 +1584,7 @@ void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1606,7 +1606,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { return size; } void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->current_temperature); buffer.encode_float(3, this->target_temperature); buffer.encode_uint32(4, static_cast(this->mode)); @@ -1619,7 +1619,7 @@ void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t WaterHeaterStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->current_temperature); size += ProtoSize::calc_float(1, this->target_temperature); size += ProtoSize::calc_uint32(1, static_cast(this->mode)); @@ -1675,7 +1675,7 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1695,7 +1695,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesNumberResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1714,7 +1714,7 @@ uint32_t ListEntitiesNumberResponse::calculate_size() const { return size; } void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -1723,7 +1723,7 @@ void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t NumberStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->state); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1760,7 +1760,7 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1777,7 +1777,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSelectResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1795,7 +1795,7 @@ uint32_t ListEntitiesSelectResponse::calculate_size() const { return size; } void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -1804,7 +1804,7 @@ void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SelectStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -1849,7 +1849,7 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1868,7 +1868,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesSirenResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1888,7 +1888,7 @@ uint32_t ListEntitiesSirenResponse::calculate_size() const { return size; } void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -1896,7 +1896,7 @@ void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t SirenStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -1961,7 +1961,7 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1979,7 +1979,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesLockResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -1996,7 +1996,7 @@ uint32_t ListEntitiesLockResponse::calculate_size() const { return size; } void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -2004,7 +2004,7 @@ void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t LockStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast(this->state)); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -2054,7 +2054,7 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2069,7 +2069,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesButtonResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2124,7 +2124,7 @@ uint32_t MediaPlayerSupportedFormat::calculate_size() const { } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2143,7 +2143,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2163,7 +2163,7 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { return size; } void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); buffer.encode_bool(4, this->muted); @@ -2173,7 +2173,7 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t MediaPlayerStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast(this->state)); size += ProtoSize::calc_float(1, this->volume); size += ProtoSize::calc_bool(1, this->muted); @@ -2942,7 +2942,7 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2959,7 +2959,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) con uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -2975,7 +2975,7 @@ uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { return size; } void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -2983,7 +2983,7 @@ void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t AlarmControlPanelStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_uint32(1, static_cast(this->state)); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -3030,7 +3030,7 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3048,7 +3048,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTextResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3065,7 +3065,7 @@ uint32_t ListEntitiesTextResponse::calculate_size() const { return size; } void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES @@ -3074,7 +3074,7 @@ void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TextStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->state.size()); size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES @@ -3119,7 +3119,7 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3133,7 +3133,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesDateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3146,7 +3146,7 @@ uint32_t ListEntitiesDateResponse::calculate_size() const { return size; } void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->year); buffer.encode_uint32(4, this->month); @@ -3157,7 +3157,7 @@ void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t DateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_uint32(1, this->year); size += ProtoSize::calc_uint32(1, this->month); @@ -3202,7 +3202,7 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3216,7 +3216,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesTimeResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3229,7 +3229,7 @@ uint32_t ListEntitiesTimeResponse::calculate_size() const { return size; } void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->hour); buffer.encode_uint32(4, this->minute); @@ -3240,7 +3240,7 @@ void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t TimeStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_uint32(1, this->hour); size += ProtoSize::calc_uint32(1, this->minute); @@ -3285,7 +3285,7 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3303,7 +3303,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesEventResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3322,7 +3322,7 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { return size; } void EventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); @@ -3330,7 +3330,7 @@ void EventResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t EventResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->event_type.size()); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); @@ -3341,7 +3341,7 @@ uint32_t EventResponse::calculate_size() const { #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3359,7 +3359,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesValveResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3376,7 +3376,7 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { return size; } void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); #ifdef USE_DEVICES @@ -3385,7 +3385,7 @@ void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t ValveStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_float(1, this->position); size += ProtoSize::calc_uint32(1, static_cast(this->current_operation)); #ifdef USE_DEVICES @@ -3428,7 +3428,7 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3442,7 +3442,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesDateTimeResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3455,7 +3455,7 @@ uint32_t ListEntitiesDateTimeResponse::calculate_size() const { return size; } void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); #ifdef USE_DEVICES @@ -3464,7 +3464,7 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t DateTimeStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_fixed32(1, this->epoch_seconds); #ifdef USE_DEVICES @@ -3501,7 +3501,7 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3516,7 +3516,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesUpdateResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3530,7 +3530,7 @@ uint32_t ListEntitiesUpdateResponse::calculate_size() const { return size; } void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_fixed32(1, this->key); + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_bool(3, this->in_progress); buffer.encode_bool(4, this->has_progress); @@ -3546,7 +3546,7 @@ void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { } uint32_t UpdateStateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_bool(1, this->missing_state); size += ProtoSize::calc_bool(1, this->in_progress); size += ProtoSize::calc_bool(1, this->has_progress); @@ -3642,7 +3642,7 @@ uint32_t ZWaveProxyRequest::calculate_size() const { #ifdef USE_INFRARED void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); @@ -3657,7 +3657,7 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->object_id.size()); - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON size += ProtoSize::calc_length(1, this->icon.size()); @@ -3717,7 +3717,7 @@ void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(1, this->device_id); #endif - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); for (const auto &it : *this->timings) { buffer.encode_sint32(3, it, true); } @@ -3727,7 +3727,7 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif - size += ProtoSize::calc_fixed32(1, this->key); + size += 5; if (!this->timings->empty()) { for (const auto &it : *this->timings) { size += ProtoSize::calc_sint32_force(1, it); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index f2f7fa5238..d86cf912db 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -1,6 +1,7 @@ // This file was automatically generated with a tool. // See script/api_protobuf/api_protobuf.py #include "api_pb2_service.h" +#include "api_connection.h" #include "esphome/core/log.h" namespace esphome::api { @@ -20,7 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) { } #endif -void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { +void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements switch (msg_type) { case HelloRequest::MESSAGE_TYPE: // No setup required diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 10fd88d8e1..4925a6497a 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -8,7 +8,7 @@ namespace esphome::api { -class APIServerConnectionBase : public ProtoService { +class APIServerConnectionBase { public: #ifdef HAS_PROTO_MESSAGE_DUMP protected: @@ -19,227 +19,223 @@ class APIServerConnectionBase : public ProtoService { public: #endif - virtual void on_hello_request(const HelloRequest &value){}; + void on_hello_request(const HelloRequest &value){}; - virtual void on_disconnect_request(){}; - virtual void on_disconnect_response(){}; - virtual void on_ping_request(){}; - virtual void on_ping_response(){}; - virtual void on_device_info_request(){}; + void on_disconnect_request(){}; + void on_disconnect_response(){}; + void on_ping_request(){}; + void on_ping_response(){}; + void on_device_info_request(){}; - virtual void on_list_entities_request(){}; + void on_list_entities_request(){}; - virtual void on_subscribe_states_request(){}; + void on_subscribe_states_request(){}; #ifdef USE_COVER - virtual void on_cover_command_request(const CoverCommandRequest &value){}; + void on_cover_command_request(const CoverCommandRequest &value){}; #endif #ifdef USE_FAN - virtual void on_fan_command_request(const FanCommandRequest &value){}; + void on_fan_command_request(const FanCommandRequest &value){}; #endif #ifdef USE_LIGHT - virtual void on_light_command_request(const LightCommandRequest &value){}; + void on_light_command_request(const LightCommandRequest &value){}; #endif #ifdef USE_SWITCH - virtual void on_switch_command_request(const SwitchCommandRequest &value){}; + void on_switch_command_request(const SwitchCommandRequest &value){}; #endif - virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; + void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; #ifdef USE_API_NOISE - virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void on_subscribe_homeassistant_services_request(){}; + void on_subscribe_homeassistant_services_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; + void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_subscribe_home_assistant_states_request(){}; + void on_subscribe_home_assistant_states_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; + void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; #endif - virtual void on_get_time_response(const GetTimeResponse &value){}; + void on_get_time_response(const GetTimeResponse &value){}; #ifdef USE_API_USER_DEFINED_ACTIONS - virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; + void on_execute_service_request(const ExecuteServiceRequest &value){}; #endif #ifdef USE_CAMERA - virtual void on_camera_image_request(const CameraImageRequest &value){}; + void on_camera_image_request(const CameraImageRequest &value){}; #endif #ifdef USE_CLIMATE - virtual void on_climate_command_request(const ClimateCommandRequest &value){}; + void on_climate_command_request(const ClimateCommandRequest &value){}; #endif #ifdef USE_WATER_HEATER - virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; + void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; #endif #ifdef USE_NUMBER - virtual void on_number_command_request(const NumberCommandRequest &value){}; + void on_number_command_request(const NumberCommandRequest &value){}; #endif #ifdef USE_SELECT - virtual void on_select_command_request(const SelectCommandRequest &value){}; + void on_select_command_request(const SelectCommandRequest &value){}; #endif #ifdef USE_SIREN - virtual void on_siren_command_request(const SirenCommandRequest &value){}; + void on_siren_command_request(const SirenCommandRequest &value){}; #endif #ifdef USE_LOCK - virtual void on_lock_command_request(const LockCommandRequest &value){}; + void on_lock_command_request(const LockCommandRequest &value){}; #endif #ifdef USE_BUTTON - virtual void on_button_command_request(const ButtonCommandRequest &value){}; + void on_button_command_request(const ButtonCommandRequest &value){}; #endif #ifdef USE_MEDIA_PLAYER - virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; + void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_le_advertisements_request( - const SubscribeBluetoothLEAdvertisementsRequest &value){}; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; + void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_connections_free_request(){}; + void on_subscribe_bluetooth_connections_free_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_unsubscribe_bluetooth_le_advertisements_request(){}; + void on_unsubscribe_bluetooth_le_advertisements_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){}; + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_response(const VoiceAssistantResponse &value){}; + void on_voice_assistant_response(const VoiceAssistantResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; + void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; + void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; #endif #ifdef USE_ALARM_CONTROL_PANEL - virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; #endif #ifdef USE_TEXT - virtual void on_text_command_request(const TextCommandRequest &value){}; + void on_text_command_request(const TextCommandRequest &value){}; #endif #ifdef USE_DATETIME_DATE - virtual void on_date_command_request(const DateCommandRequest &value){}; + void on_date_command_request(const DateCommandRequest &value){}; #endif #ifdef USE_DATETIME_TIME - virtual void on_time_command_request(const TimeCommandRequest &value){}; + void on_time_command_request(const TimeCommandRequest &value){}; #endif #ifdef USE_VALVE - virtual void on_valve_command_request(const ValveCommandRequest &value){}; + void on_valve_command_request(const ValveCommandRequest &value){}; #endif #ifdef USE_DATETIME_DATETIME - virtual void on_date_time_command_request(const DateTimeCommandRequest &value){}; + void on_date_time_command_request(const DateTimeCommandRequest &value){}; #endif #ifdef USE_UPDATE - virtual void on_update_command_request(const UpdateCommandRequest &value){}; + void on_update_command_request(const UpdateCommandRequest &value){}; #endif #ifdef USE_ZWAVE_PROXY - virtual void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; #endif #ifdef USE_ZWAVE_PROXY - virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; + void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif #ifdef USE_IR_RF - virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; + void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; #endif #ifdef USE_SERIAL_PROXY - virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; + void on_serial_proxy_request(const SerialProxyRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; #endif - - protected: - void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; } // namespace esphome::api diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 2fd9cb0dd2..c2b0f14bcc 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -136,8 +136,9 @@ class CustomAPIDevice { template void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), std::move(f)); + auto *obj = static_cast(this); + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), + [obj, callback](StringRef state) { (obj->*callback)(state); }); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). @@ -148,10 +149,12 @@ class CustomAPIDevice { ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, std::placeholders::_1); + auto *obj = static_cast(this); // Explicit type to disambiguate overload resolution - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), - std::function(f)); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + std::function( + [obj, callback](const std::string &state) { (obj->*callback)(state); })); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant. @@ -176,8 +179,10 @@ class CustomAPIDevice { template void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), std::move(f)); + auto *obj = static_cast(this); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + [obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); }); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). @@ -188,10 +193,12 @@ class CustomAPIDevice { ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); + auto *obj = static_cast(this); // Explicit type to disambiguate overload resolution - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), - std::function(f)); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + std::function( + [obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); })); } #else template diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d6e993d3a5..a1d825ead9 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -236,6 +236,21 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + /// Write a precomputed tag byte + 32-bit value in one operation. + /// Tag must be a single-byte varint (< 128). No zero check. + inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(5); + this->pos_[0] = tag; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(this->pos_ + 1, &value, 4); +#else + this->pos_[1] = static_cast(value & 0xFF); + this->pos_[2] = static_cast((value >> 8) & 0xFF); + this->pos_[3] = static_cast((value >> 16) & 0xFF); + this->pos_[4] = static_cast((value >> 24) & 0xFF); +#endif + this->pos_ += 5; + } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) return; @@ -276,8 +291,7 @@ class ProtoWriteBuffer { this->debug_check_bounds_(1); *this->pos_++ = value ? 0x01 : 0x00; } - // noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy - __attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; @@ -697,26 +711,7 @@ inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) template const char *proto_enum_to_string(T value); -class ProtoService { - public: - protected: - virtual bool is_authenticated() = 0; - virtual bool is_connection_setup() = 0; - virtual void on_fatal_error() = 0; - virtual void on_no_setup_connection() = 0; - virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; - virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - - // Authentication helper methods - inline bool check_connection_setup_() { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return false; - } - return true; - } - - inline bool check_authenticated_() { return this->check_connection_setup_(); } -}; +// ProtoService removed — its methods were inlined into APIConnection. +// APIConnection is the concrete server-side implementation; the extra virtual layer was unnecessary. } // namespace esphome::api diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f8b130e08a..f30f9d3279 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -96,8 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component { void setup() override { this->last_state_ = this->parent_->get_state_default(false); - auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1); - this->parent_->add_on_state_callback(f); + this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); }); } float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 2f60f81471..71d98332e0 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -103,17 +103,17 @@ size_t BLENUS::available() { #endif } -uart::FlushResult BLENUS::flush() { +uart::UARTFlushResult BLENUS::flush() { constexpr uint32_t timeout_500ms = 500; uint32_t start = millis(); while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { if (millis() - start > timeout_500ms) { ESP_LOGW(TAG, "Flush timeout"); - return uart::FlushResult::TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; } delay(1); } - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } void BLENUS::connected(bt_conn *conn, uint8_t err) { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index b482c240e5..f1afd54af9 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -26,7 +26,7 @@ class BLENUS : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 0210d1e67d..ed2ec80896 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -279,7 +279,8 @@ void BME68xBSEC2Component::run_() { uint32_t meas_dur = 0; meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); - this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); }); + this->trigger_time_ns_ = curr_time_ns; + this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); } else { ESP_LOGV(TAG, "Measurement not required"); this->read_(curr_time_ns); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 8f4d8f61c2..1ed72eee03 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -116,6 +116,8 @@ class BME68xBSEC2Component : public Component { int8_t bme68x_status_{BME68X_OK}; int64_t last_time_ms_{0}; + int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit + // toolchains with small std::function SBO uint32_t millis_overflow_counter_{0}; std::queue> queue_; diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 2f0bd26a02..b858eee4ee 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -4,8 +4,6 @@ #include "esphome/core/hal.h" #include -#include -#include namespace esphome { namespace combination { @@ -20,12 +18,12 @@ void CombinationComponent::log_config_(const LogString *combo_type) { void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &stddev) { - this->sensor_pairs_.emplace_back(sensor, stddev); +void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &compute) { + this->sensor_sources_.push_back({sensor, compute, this}); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) { - this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); +void CombinationOneParameterComponent::add_source(Sensor *sensor, float value) { + this->add_source(sensor, std::function{[value](float x) -> float { return value; }}); } void CombinationNoParameterComponent::log_source_sensors() { @@ -37,9 +35,8 @@ void CombinationNoParameterComponent::log_source_sensors() { void CombinationOneParameterComponent::log_source_sensors() { ESP_LOGCONFIG(TAG, " Source Sensors:"); - for (const auto &sensor : this->sensor_pairs_) { - auto &entity = *sensor.first; - ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str()); + for (const auto &source : this->sensor_sources_) { + ESP_LOGCONFIG(TAG, " - %s", source.sensor->get_name().c_str()); } } @@ -62,9 +59,12 @@ void KalmanCombinationComponent::dump_config() { } void KalmanCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { - const auto stddev = sensor.second; - sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + for (auto &source : this->sensor_sources_) { + // [&source] is safe: source refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + source.sensor->add_on_state_callback([&source](float x) -> void { + static_cast(source.parent)->correct_(x, source.compute(x)); + }); } } @@ -117,10 +117,10 @@ void KalmanCombinationComponent::correct_(float value, float stddev) { } void LinearCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { + for (auto &source : this->sensor_sources_) { // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result // repeatedly in the same loop if multiple source senors update. - sensor.first->add_on_state_callback( + source.sensor->add_on_state_callback( [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); } } @@ -133,10 +133,10 @@ void LinearCombinationComponent::handle_new_value(float value) { float sum = 0.0; - for (const auto &sensor : this->sensor_pairs_) { - const float sensor_state = sensor.first->state; + for (const auto &source : this->sensor_sources_) { + const float sensor_state = source.sensor->state; if (std::isfinite(sensor_state)) { - sum += sensor_state * sensor.second(sensor_state); + sum += sensor_state * source.compute(sensor_state); } } diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index fb5e156da9..463eedc564 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -1,9 +1,10 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" -#include +#include namespace esphome { namespace combination { @@ -41,14 +42,21 @@ class CombinationNoParameterComponent : public CombinationComponent { // Base class for opertions that require one parameter to compute the combination class CombinationOneParameterComponent : public CombinationComponent { public: - void add_source(Sensor *sensor, std::function const &stddev); - void add_source(Sensor *sensor, float stddev); + void set_source_count(size_t count) { this->sensor_sources_.init(count); } + void add_source(Sensor *sensor, std::function const &compute); + void add_source(Sensor *sensor, float value); - /// @brief Logs all source sensor's names in sensor_pairs_ + /// @brief Logs all source sensors' names in sensor_sources_ void log_source_sensors() override; protected: - std::vector>> sensor_pairs_; + struct SensorSource { + sensor::Sensor *sensor; + std::function compute; + CombinationOneParameterComponent *parent; + }; + + FixedVector sensor_sources_; }; class KalmanCombinationComponent : public CombinationOneParameterComponent { diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index 0204162e8d..327cedee1e 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -180,6 +180,9 @@ async def to_code(config): if proces_std_dev := config.get(CONF_PROCESS_STD_DEV): cg.add(var.set_process_std_dev(proces_std_dev)) + if config[CONF_TYPE] in (CONF_KALMAN, CONF_LINEAR): + cg.add(var.set_source_count(len(config[CONF_SOURCES]))) + for source_conf in config[CONF_SOURCES]: source = await cg.get_variable(source_conf[CONF_SOURCE]) if config[CONF_TYPE] == CONF_KALMAN: diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 12ec46725d..f121e5c2d6 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -105,17 +105,18 @@ template using CoverIsClosedCondition = CoverPositionCondition class CoverPositionTrigger : public Trigger<> { public: - CoverPositionTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->position != this->last_position_) { - this->last_position_ = a_cover->position; - if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) + CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + if (this->cover_->position != this->last_position_) { + this->last_position_ = this->cover_->position; + if (this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) this->trigger(); } }); } protected: + Cover *cover_; float last_position_{NAN}; }; @@ -124,9 +125,9 @@ using CoverClosedTrigger = CoverPositionTrigger; template class CoverTrigger : public Trigger<> { public: - CoverTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - auto current_op = a_cover->current_operation; + CoverTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + auto current_op = this->cover_->current_operation; if (current_op == OP) { if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) { this->trigger(); @@ -137,6 +138,7 @@ template class CoverTrigger : public Trigger<> { } protected: + Cover *cover_; optional last_operation_{}; }; } // namespace esphome::cover diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index 98f23aa713..6c0a33c842 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -33,9 +33,12 @@ class DateTimeBase : public EntityBase { class DateTimeStateTrigger : public Trigger { public: - explicit DateTimeStateTrigger(DateTimeBase *parent) { - parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); }); + explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) { + parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); }); } + + protected: + DateTimeBase *parent_; }; } // namespace esphome::datetime diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index 9c64794cef..50c26c344c 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -96,37 +96,52 @@ template class IsActiveCondition : public Condition { class DisplayMenuOnEnterTrigger : public Trigger { public: - explicit DisplayMenuOnEnterTrigger(MenuItem *parent) { - parent->add_on_enter_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_enter_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnLeaveTrigger : public Trigger { public: - explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) { - parent->add_on_leave_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_leave_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnValueTrigger : public Trigger { public: - explicit DisplayMenuOnValueTrigger(MenuItem *parent) { - parent->add_on_value_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_value_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnNextTrigger : public Trigger { public: - explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) { - parent->add_on_next_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_next_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; class DisplayMenuOnPrevTrigger : public Trigger { public: - explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) { - parent->add_on_prev_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_prev_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; } // namespace display_menu_base diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index aa0a77cdd8..b1366cd069 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -127,8 +127,7 @@ void DPS310Component::read_() { this->update_in_progress_ = false; this->status_clear_warning(); } else { - auto f = std::bind(&DPS310Component::read_, this); - this->set_timeout("dps310", 10, f); + this->set_timeout("dps310", 10, [this]() { this->read_(); }); } } diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 18178c83ff..f85f13fe73 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -28,6 +28,7 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, CONF_REF, CONF_SAFE_MODE, + CONF_SIZE, CONF_SOURCE, CONF_TYPE, CONF_VARIANT, @@ -96,6 +97,7 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SUBTYPE = "subtype" ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" @@ -1258,6 +1260,43 @@ def _set_default_framework(config): return config +RESERVED_PARTITION_NAMES = { + "nvs", + "app0", + "app1", + "otadata", + "eeprom", + "spiffs", + "phy_init", +} + +VALID_APP_SUBTYPES = {"factory", "test"} +VALID_DATA_SUBTYPES = { + "nvs", + "nvs_keys", + "spiffs", + "coredump", + "efuse", + "fat", + "undefined", + "littlefs", +} + + +def _validate_custom_partition(config: ConfigType) -> ConfigType: + """Voluptuous validator for custom partition schema.""" + try: + _validate_partition( + config[CONF_NAME], + config[CONF_TYPE], + config[CONF_SUBTYPE], + config[CONF_SIZE], + ) + except ValueError as e: + raise cv.Invalid(str(e)) from e + return config + + FLASH_SIZES = [ "2MB", "4MB", @@ -1280,7 +1319,28 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), - cv.Optional(CONF_PARTITIONS): cv.file_, + cv.Optional(CONF_PARTITIONS): cv.Any( + cv.file_, + cv.ensure_list( + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_TYPE): cv.All( + cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)), + cv.int_to_hex_string, + ), + cv.Required(CONF_SUBTYPE): cv.All( + cv.Any(cv.string_strict, cv.int_range(0, 0xFE)), + cv.int_to_hex_string, + ), + cv.Required(CONF_SIZE): cv.int_range(min=0x1000), + } + ), + _validate_custom_partition, + ), + ), + ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, } @@ -1749,9 +1809,18 @@ async def to_code(config): if use_platformio: cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config: - add_extra_build_file( - "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) - ) + if isinstance(config[CONF_PARTITIONS], list): + for partition in config[CONF_PARTITIONS]: + add_partition( + partition[CONF_NAME], + partition[CONF_TYPE], + partition[CONF_SUBTYPE], + partition[CONF_SIZE], + ) + else: + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) if assertion_level := advanced.get(CONF_ASSERTION_LEVEL): for key, flag in ASSERTION_LEVELS.items(): @@ -1851,6 +1920,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False) add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False) + # Disable PicolibC Newlib compatibility shim on IDF 6.0+ + # IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local + # stdin/stdout/stderr and getreent() for code compiled against Newlib. + # ESPHome doesn't link against Newlib-built libraries that use stdio. + # If a component needs it (e.g. precompiled Newlib binaries), re-enable via: + # esp32: + # framework: + # sdkconfig_options: + # CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y" + if idf_version() >= cv.Version(6, 0, 0): + add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False) + # Disable regi2c control functions in IRAM # Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: @@ -1885,45 +1966,175 @@ async def to_code(config): CORE.add_job(_write_arduino_libraries_sdkconfig) -APP_PARTITION_SIZES = { - "2MB": 0x0C0000, # 768 KB - "4MB": 0x1C0000, # 1792 KB - "8MB": 0x3C0000, # 3840 KB - "16MB": 0x7C0000, # 7936 KB - "32MB": 0xFC0000, # 16128 KB +KEY_CUSTOM_PARTITIONS = "custom_partitions" + + +@dataclass +class PartitionEntry: + name: str + type: str + subtype: str + size: int + + +# Partition sizes (offsets auto-placed by gen_esp32part.py). +# These constants are the single source of truth — used in both +# the CSV generation and the overhead calculation. +BOOTLOADER_SIZE = 0x8000 +PARTITION_TABLE_SIZE = 0x1000 +FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE +OTADATA_SIZE = 0x2000 +PHY_INIT_SIZE = 0x1000 +EEPROM_SIZE = 0x1000 # Arduino only +SPIFFS_SIZE = 0xF000 # Arduino only +ARDUINO_NVS_SIZE = 0x60000 +IDF_NVS_SIZE = 0x70000 + + +def _get_partition_overhead() -> int: + """Total non-app partition budget (system partitions + nvs + padding). + + Custom partitions are appended at the end and steal from app. + """ + # otadata + phy_init are followed by app0 which requires 64KB alignment, + # so pad up to the next 64KB boundary. + overhead = ( + FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF + ) & ~0xFFFF + if CORE.using_arduino: + overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE + else: + overhead += IDF_NVS_SIZE + return overhead + + +VALID_SUBTYPES: dict[str, set[str]] = { + "app": VALID_APP_SUBTYPES, + "data": VALID_DATA_SUBTYPES, } -def get_arduino_partition_csv(flash_size: str): - app_partition_size = APP_PARTITION_SIZES[flash_size] - eeprom_partition_size = 0x1000 # 4 KB - spiffs_partition_size = 0xF000 # 60 KB - - app0_partition_start = 0x010000 # 64 KB - app1_partition_start = app0_partition_start + app_partition_size - eeprom_partition_start = app1_partition_start + app_partition_size - spiffs_partition_start = eeprom_partition_start + eeprom_partition_size - - return f"""\ -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xE000, 0x2000, -app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, -app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, -eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, -spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} -""" +def _validate_partition( + name: str, p_type: str | int, subtype: str | int, size: int +) -> None: + """Validate partition parameters. Raises ValueError on invalid input.""" + if name in RESERVED_PARTITION_NAMES: + raise ValueError(f"Partition name '{name}' is reserved.") + if size % 0x1000 != 0: + raise ValueError("Partition size must be 4KB (0x1000) aligned.") + # Numeric or already-normalized hex types/subtypes skip string validation + if not isinstance(p_type, str) or p_type.startswith("0x"): + return + if p_type not in VALID_SUBTYPES: + raise ValueError( + f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed." + " Use numbers for custom types." + ) + if not isinstance(subtype, str) or subtype.startswith("0x"): + return + valid = VALID_SUBTYPES[p_type] + if subtype not in valid: + raise ValueError( + f"Subtype '{subtype}' is invalid for {p_type} type." + f" Only {', '.join(sorted(valid))} are allowed." + " Use numbers for custom subtypes." + ) -def get_idf_partition_csv(flash_size: str): - app_partition_size = APP_PARTITION_SIZES[flash_size] +def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None: + """Register a custom partition to be appended to the partition table. - return f"""\ -otadata, data, ota, , 0x2000, -phy_init, data, phy, , 0x1000, -app0, app, ota_0, , 0x{app_partition_size:X}, -app1, app, ota_1, , 0x{app_partition_size:X}, -nvs, data, nvs, , 0x6D000, -""" + Called from component to_code() to request additional flash partitions. + Size must be 4KB aligned. Integer types/subtypes are converted to hex strings. + """ + if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}): + raise ValueError(f"Partition name '{name}' is already defined.") + _validate_partition(name, p_type, subtype, size) + p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type + subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype + custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {}) + custom_partitions[name] = PartitionEntry( + name=name, type=p_type_str, subtype=subtype_str, size=size + ) + + +def _flash_size_to_bytes(flash_size_mb: str) -> int: + """Convert flash size string (e.g. '4MB') to bytes.""" + return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024 + + +def _get_custom_partitions_total_size() -> int: + """Total size of custom partitions including alignment padding.""" + size = 0 + for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values(): + if partition.type == "app": + size = (size + 0xFFFF) & ~0xFFFF # align to 64KB + size += partition.size + return size + + +def _get_app_partition_size(flash_size_mb: str) -> int: + flash_bytes = _flash_size_to_bytes(flash_size_mb) + custom_total = _get_custom_partitions_total_size() + # Align down to 64KB — app partitions require 64KB-aligned offsets, + # so the size must also be aligned to avoid unbudgeted padding. + raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2 + app_size = raw_size & ~0xFFFF + wasted = (raw_size - app_size) * 2 + if wasted: + _LOGGER.info( + "Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.", + wasted // 1024, + ) + if app_size <= 0x10000: # 64 KB + raise ValueError( + "Custom partitions are too large to fit in the available flash size. " + "Reduce custom partition sizes." + ) + if app_size <= 0x80000: # 512 KB + _LOGGER.warning( + "App partition size is only %dKB. This may be too small for firmware with " + "many components. Consider reducing custom partition sizes or using a " + "larger flash chip.", + app_size // 1024, + ) + return app_size + + +def get_partition_csv(flash_size_mb: str) -> str: + app_size = _get_app_partition_size(flash_size_mb) + + partitions: list[PartitionEntry] = [ + PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE), + PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE), + PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size), + PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size), + ] + if CORE.using_arduino: + partitions.append( + PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE) + ) + partitions.append( + PartitionEntry( + name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE + ) + ) + partitions.append( + PartitionEntry( + name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE + ) + ) + else: + partitions.append( + PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE) + ) + partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values()) + + csv = "".join( + f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions + ) + _LOGGER.debug("Partition table:\n%s", csv) + return csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: @@ -2030,16 +2241,10 @@ def copy_files(): if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE] - if CORE.using_arduino: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv(flash_size), - ) - else: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_idf_partition_csv(flash_size), - ) + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_partition_csv(flash_size), + ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, # and no version.txt file exists, the CMake script fails for some setups. diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 83bd09b643..313818e601 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "crash_handler.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -15,7 +16,6 @@ #include void setup(); // NOLINT(readability-redundant-declaration) -void loop(); // NOLINT(readability-redundant-declaration) // Weak stub for initArduino - overridden when the Arduino component is present extern "C" __attribute__((weak)) void initArduino() {} @@ -65,7 +65,7 @@ TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non- void loop_task(void *pv_params) { setup(); while (true) { - loop(); + App.loop(); } } diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index b3166cf822..fec257f90b 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -28,7 +28,11 @@ def esp32_validate_gpio_pin(value: int) -> int: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") if value in _ESP_SDIO_PINS: raise cv.Invalid( - f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" + f"This pin cannot be used on ESP32s and is already used by the flash interface" + f" (function: {_ESP_SDIO_PINS[value]})." + f" If you are using an ESP32 module that uses a different flash pin" + f" configuration (e.g. ESP32-PICO-V3-02), you can set" + f" 'ignore_pin_validation_error: true' to bypass this check." ) if 9 <= value <= 10: _LOGGER.warning( diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 76f1c59c73..afcec8bfc7 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -9,12 +9,14 @@ #include #include +#include "esphome/core/log.h" #include "esp_random.h" #include "esp_system.h" namespace esphome { -uint32_t random_uint32() { return esp_random(); } +static const char *const TAG = "esp32"; + bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; @@ -64,22 +66,43 @@ LwIPLock::~LwIPLock() { #endif } +/// Read MAC and validate both the return code and content. +static bool read_valid_mac(uint8_t *mac, esp_err_t err) { return err == ESP_OK && mac_address_is_valid(mac); } + +static constexpr size_t MAC_ADDRESS_SIZE_BITS = MAC_ADDRESS_SIZE * 8; // 48 bits + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. - if (has_custom_mac_address()) { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); - } else { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + // Both paths already read raw eFuse bytes, so there is no CRC-bypass fallback + // (unlike the non-IEEE802154 path where esp_efuse_mac_get_default does CRC checks). + if (has_custom_mac_address() && + read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS))) { + return; + } + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + return; } #else - if (has_custom_mac_address()) { - esp_efuse_mac_get_custom(mac); - } else { - esp_efuse_mac_get_default(mac); + if (has_custom_mac_address() && read_valid_mac(mac, esp_efuse_mac_get_custom(mac))) { + return; + } + if (read_valid_mac(mac, esp_efuse_mac_get_default(mac))) { + return; + } + // Default MAC read failed (e.g., eFuse CRC error) - try reading raw eFuse bytes + // directly, bypassing CRC validation. A MAC that passes mac_address_is_valid() + // (non-zero, non-broadcast, unicast) is almost certainly the real factory MAC + // with a corrupted CRC byte, which is far better than returning garbage or zeros. + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + ESP_LOGW(TAG, "eFuse MAC CRC failed but raw bytes appear valid - using raw eFuse MAC"); + return; } #endif + // All methods failed - zero the MAC rather than returning garbage + ESP_LOGE(TAG, "Failed to read a valid MAC address from eFuse"); + memset(mac, 0, MAC_ADDRESS_SIZE); } void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } @@ -89,9 +112,11 @@ bool has_custom_mac_address() { uint8_t mac[6]; // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails #ifndef USE_ESP32_VARIANT_ESP32 - return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #else - return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #endif #else return false; diff --git a/esphome/components/esp32/iram_fix.py.script b/esphome/components/esp32/iram_fix.py.script index 0d23f9a81b..b656d7d1a6 100644 --- a/esphome/components/esp32/iram_fix.py.script +++ b/esphome/components/esp32/iram_fix.py.script @@ -4,12 +4,40 @@ import re # pylint: disable=E0602 Import("env") # noqa -# IRAM size for testing mode (2MB - large enough to accommodate grouped tests) -TESTING_IRAM_SIZE = 0x200000 +# Memory sizes for testing mode (large enough to accommodate grouped tests) +TESTING_IRAM_SIZE = 0x200000 # 2MB +TESTING_DRAM_SIZE = 0x200000 # 2MB + + +def patch_segment(content, segment_name, new_size): + """Patch a memory segment's length in linker script content. + + Handles both single-line and multi-line segment definitions, e.g.: + iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 + or split across lines: + dram0_0_seg (RW) : org = 0x3FFB0000 + 0xdb5c, + len = 0x2c200 - 0xdb5c + + Args: + content: Full linker script content as string + segment_name: Name of the segment (e.g., 'iram0_0_seg') + new_size: New size as integer + + Returns: + Tuple of (new_content, was_patched) + """ + # Match segment name through to "len = " allowing newlines between org and len + pattern = rf'({re.escape(segment_name)}\s*\([^)]*\)\s*:\s*org\s*=\s*.+?,\s*len\s*=\s*)(\S+[^\n]*)' + if match := re.search(pattern, content, re.DOTALL): + replacement = f"{match.group(1)}{new_size:#x}" + new_content = content[:match.start()] + replacement + content[match.end():] + if new_content != content: + return new_content, True + return content, False def patch_idf_linker_script(source, target, env): - """Patch ESP-IDF linker script to increase IRAM size for testing mode.""" + """Patch ESP-IDF linker script to increase IRAM and DRAM size for testing mode.""" # Check if we're in testing mode by looking for the define build_flags = env.get("BUILD_FLAGS", []) testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) @@ -34,36 +62,22 @@ def patch_idf_linker_script(source, target, env): print(f"ESPHome: Error reading linker script: {e}") return - # Check if this file contains iram0_0_seg - if 'iram0_0_seg' not in content: - print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}") - return + patches = [] - # Look for iram0_0_seg definition and increase its length - # ESP-IDF format can be: - # iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 - # or more complex with nested parentheses: - # iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000) - # We want to change len to TESTING_IRAM_SIZE for testing + content, patched = patch_segment(content, 'iram0_0_seg', TESTING_IRAM_SIZE) + if patched: + patches.append(f"IRAM={TESTING_IRAM_SIZE:#x}") - # Use a more robust approach: find the line and manually parse it - lines = content.split('\n') - for i, line in enumerate(lines): - if 'iram0_0_seg' in line and 'len' in line: - # Find the position of "len = " and replace everything after it until the end of the statement - match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line) - if match: - lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}" - break + content, patched = patch_segment(content, 'dram0_0_seg', TESTING_DRAM_SIZE) + if patched: + patches.append(f"DRAM={TESTING_DRAM_SIZE:#x}") - updated = '\n'.join(lines) - - if updated != content: + if patches: with open(memory_ld, "w") as f: - f.write(updated) - print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode") + f.write(content) + print(f"ESPHome: Patched {', '.join(patches)} in {memory_ld} for testing mode") else: - print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}") + print(f"ESPHome: Warning - could not patch memory segments in {memory_ld}") # Hook into the build process before linking diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 7260bf54e0..e88ace3e6b 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -10,7 +10,7 @@ namespace esphome::esp32 { -static const char *const TAG = "esp32.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/esp32_improv/automation.h index 52c5da125b..cd2bd84c30 100644 --- a/esphome/components/esp32_improv/automation.h +++ b/esphome/components/esp32_improv/automation.h @@ -12,58 +12,73 @@ namespace esp32_improv { class ESP32ImprovProvisionedTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovProvisioningTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONING && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStartTrigger : public Trigger<> { public: - explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && - !parent->is_failed()) { - trigger(); + !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStateTrigger : public Trigger { public: - explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (!parent->is_failed()) { - trigger(state, error); + explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (!this->parent_->is_failed()) { + this->trigger(state, error); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStoppedTrigger : public Trigger<> { public: - explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_STOPPED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; } // namespace esp32_improv diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index e4ae49f235..c24b08b06f 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -350,8 +350,7 @@ void ESP32ImprovComponent::process_incoming_data_() { ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this); - this->set_timeout("wifi-connect-timeout", 30000, f); + this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); this->incoming_data_.clear(); break; } diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e7124ce92f..0d331b29d6 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() { } // Publish initial OFF state for sensors that haven't received events yet + bool all_initial_published = true; for (auto *child : this->children_) { this->publish_initial_state_if_needed_(child, now); + if (!child->initial_state_published_) { + all_initial_published = false; + } } - if (!this->setup_mode_) { + // Only disable loop once all initial states are published + if (!this->setup_mode_ && all_initial_published) { this->disable_loop(); } } diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 0b31c53ff8..906fed2b29 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -13,7 +13,7 @@ extern "C" { namespace esphome::esp8266 { -static const char *const TAG = "esp8266.preferences"; +static const char *const TAG = "preferences"; static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 4421a1c7aa..42cb0b3cfc 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -18,12 +18,6 @@ void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } #endif -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *EthernetComponent::get_use_address() const { return this->use_address_; } - -void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } - #ifdef USE_ETHERNET_IP_STATE_LISTENERS void EthernetComponent::notify_ip_state_listeners_() { auto ips = this->get_ip_addresses(); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 88a86bc043..b6699e8020 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -103,8 +103,8 @@ class EthernetComponent final : public Component { network::IPAddresses get_ip_addresses(); network::IPAddress get_dns_address(uint8_t num); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } void get_eth_mac_address_raw(uint8_t *mac); // Remove before 2026.9.0 ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 3c3b0ce519..3ee6f89e55 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -113,16 +113,19 @@ template class FanIsOffCondition : public Condition { class FanStateTrigger : public Trigger { public: - FanStateTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { this->trigger(state); }); + FanStateTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { this->trigger(this->fan_); }); } + + protected: + Fan *fan_; }; class FanTurnOnTrigger : public Trigger<> { public: - FanTurnOnTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOnTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = is_on && !this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -133,14 +136,15 @@ class FanTurnOnTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanTurnOffTrigger : public Trigger<> { public: - FanTurnOffTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOffTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = !is_on && this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -151,14 +155,15 @@ class FanTurnOffTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanDirectionSetTrigger : public Trigger { public: - FanDirectionSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto direction = state->direction; + FanDirectionSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto direction = this->fan_->direction; auto should_trigger = direction != this->last_direction_; this->last_direction_ = direction; if (should_trigger) { @@ -169,14 +174,15 @@ class FanDirectionSetTrigger : public Trigger { } protected: + Fan *fan_; FanDirection last_direction_; }; class FanOscillatingSetTrigger : public Trigger { public: - FanOscillatingSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto oscillating = state->oscillating; + FanOscillatingSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto oscillating = this->fan_->oscillating; auto should_trigger = oscillating != this->last_oscillating_; this->last_oscillating_ = oscillating; if (should_trigger) { @@ -187,14 +193,15 @@ class FanOscillatingSetTrigger : public Trigger { } protected: + Fan *fan_; bool last_oscillating_; }; class FanSpeedSetTrigger : public Trigger { public: - FanSpeedSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto speed = state->speed; + FanSpeedSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto speed = this->fan_->speed; auto should_trigger = speed != this->last_speed_; this->last_speed_ = speed; if (should_trigger) { @@ -205,14 +212,15 @@ class FanSpeedSetTrigger : public Trigger { } protected: + Fan *fan_; int last_speed_; }; class FanPresetSetTrigger : public Trigger { public: - FanPresetSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto preset_mode = state->get_preset_mode(); + FanPresetSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto preset_mode = this->fan_->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { @@ -223,6 +231,7 @@ class FanPresetSetTrigger : public Trigger { } protected: + Fan *fan_; StringRef last_preset_mode_{}; }; diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 2667dbdbdf..c8813bf1bc 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -552,6 +552,7 @@ async def to_code(config): """ # get the codepoints from glyphsets and flatten to a set of chrs. + cg.add_define("USE_FONT") point_set: set[str] = { chr(x) for x in flatten( diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 5e3bf1dd20..ecf0ca6bdd 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -9,13 +9,87 @@ namespace font { static const char *const TAG = "font"; #ifdef USE_LVGL_FONT -const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { - auto *fe = (Font *) font->dsc; - const auto *gd = fe->get_glyph_data_(unicode_letter); +static const uint8_t OPA4_TABLE[16] = {0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255}; + +static const uint8_t OPA2_TABLE[4] = {0, 85, 170, 255}; + +const void *Font::get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf) { + const auto *font = dsc->resolved_font; + auto *const fe = (Font *) font->dsc; + + const auto *gd = fe->get_glyph_data_(dsc->gid.index); if (gd == nullptr) { return nullptr; } - return gd->data; + + const uint8_t *bitmap_in = gd->data; + uint8_t *bitmap_out_tmp = draw_buf->data; + int32_t i = 0; + int32_t x, y; + uint32_t stride = lv_draw_buf_width_to_stride(gd->width, LV_COLOR_FORMAT_A8); + + switch (fe->get_bpp()) { + case 1: { + uint8_t mask = 0; + uint8_t byte = 0; + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++) { + if (mask == 0) { + mask = 0x80; + byte = *bitmap_in++; + } + bitmap_out_tmp[x] = byte & mask ? 255 : 0; + mask >>= 1; + } + bitmap_out_tmp += stride; + } + } break; + + case 2: + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++, i++) { + switch (i & 0x3) { + default: + bitmap_out_tmp[x] = OPA2_TABLE[(*bitmap_in) >> 6]; + break; + case 1: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 4) & 0x3]; + break; + case 2: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 2) & 0x3]; + break; + case 3: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 0) & 0x3]; + bitmap_in++; + } + } + bitmap_out_tmp += stride; + } + break; + + case 4: + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++, i++) { + i = i & 0x1; + if (i == 0) { + bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) >> 4]; + } else if (i == 1) { + bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) & 0xF]; + bitmap_in++; + } + } + bitmap_out_tmp += stride; + } + break; + + case 8: + memcpy(bitmap_out_tmp, bitmap_in, gd->width * gd->height); + break; + default: + ESP_LOGD(TAG, "Unknown bpp: %d", fe->get_bpp()); + break; + } + return draw_buf; } bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { @@ -30,7 +104,8 @@ bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uin dsc->box_w = gd->width; dsc->box_h = gd->height; dsc->is_placeholder = 0; - dsc->bpp = fe->get_bpp(); + dsc->format = (lv_font_glyph_format_t) fe->get_bpp(); + dsc->gid.index = unicode_letter; return true; } diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 262ded3be4..4a09d7314d 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -90,7 +90,7 @@ class Font uint8_t bpp_; // bits per pixel #ifdef USE_LVGL_FONT lv_font_t lv_font_{}; - static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter); + static const void *get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf); static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next); const Glyph *get_glyph_data_(uint32_t unicode_letter); uint32_t last_letter_{}; diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 9043a6a493..d461fab051 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -5,6 +5,7 @@ namespace esphome { namespace gpio { static const char *const TAG = "switch.gpio"; +static constexpr uint32_t INTERLOCK_TIMEOUT_ID = 0; float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { @@ -51,7 +52,7 @@ void GPIOSwitch::write_state(bool state) { } } if (found && this->interlock_wait_time_ != 0) { - this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + this->set_timeout(INTERLOCK_TIMEOUT_ID, this->interlock_wait_time_, [this, state] { // Don't write directly, call the function again // (some other switch may have changed state while we were waiting) this->write_state(state); @@ -61,7 +62,7 @@ void GPIOSwitch::write_state(bool state) { } else if (this->interlock_wait_time_ != 0) { // If we are switched off during the interlock wait time, cancel any pending // re-activations - this->cancel_timeout("interlock"); + this->cancel_timeout(INTERLOCK_TIMEOUT_ID); } this->pin_->digital_write(state); diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h index 007889557d..ce1db18525 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.h +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -75,9 +75,12 @@ class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { public: - explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { - parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) { + parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); }); } + + protected: + GraphicalDisplayMenu *parent_; }; } // namespace graphical_display_menu diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 35eaf36d32..4a06066d3c 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -242,7 +242,7 @@ void HaierClimateBase::setup() { this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->set_phase(ProtocolPhases::SENDING_INIT_1); this->haier_protocol_.set_default_timeout_handler( - std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); + [this](haier_protocol::FrameType type) { return this->timeout_default_handler_(type); }); this->set_handlers(); this->initialization(); } diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index b7888f7976..92defe560e 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -301,32 +301,38 @@ void HonClimate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_VERSION, - std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_version_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_ID, - std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_id_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::CONTROL, - std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, - std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->status_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, - std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_management_information_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_ALARM_STATUS, - std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_alarm_status_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::REPORT_NETWORK_STATUS, - std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); - this->haier_protocol_.set_message_handler( - haier_protocol::FrameType::ALARM_STATUS, - std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->report_network_status_answer_handler_(req, msg, data, size); + }); + this->haier_protocol_.set_message_handler(haier_protocol::FrameType::ALARM_STATUS, + [this](haier_protocol::FrameType type, const uint8_t *data, size_t size) { + return this->alarm_status_message_handler_(type, data, size); + }); } void HonClimate::dump_config() { diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index e91224e2d8..2be5d13050 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -106,18 +106,21 @@ void Smartair2Climate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_VERSION, - std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_version_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::CONTROL, - std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->status_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::REPORT_NETWORK_STATUS, - std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->report_network_status_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_default_timeout_handler( - std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1)); + [this](haier_protocol::FrameType type) { return this->messages_timeout_handler_with_cycle_for_init_(type); }); } void Smartair2Climate::dump_config() { diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 00ea88ff16..da802b7fe9 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -55,15 +55,15 @@ void HomeassistantNumber::step_retrieved_(StringRef step) { } void HomeassistantNumber::setup() { - api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, + [this](StringRef state) { this->state_changed_(state); }); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); + api::global_api_server->get_home_assistant_state(this->entity_id_, "min", + [this](StringRef min) { this->min_retrieved_(min); }); + api::global_api_server->get_home_assistant_state(this->entity_id_, "max", + [this](StringRef max) { this->max_retrieved_(max); }); + api::global_api_server->get_home_assistant_state(this->entity_id_, "step", + [this](StringRef step) { this->step_retrieved_(step); }); } void HomeassistantNumber::dump_config() { diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp index fdad4f5cb6..7e8849b3e1 100644 --- a/esphome/components/host/helpers.cpp +++ b/esphome/components/host/helpers.cpp @@ -8,8 +8,6 @@ #include #endif #include -#include -#include #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -18,13 +16,6 @@ namespace esphome { static const char *const TAG = "helpers.host"; -uint32_t random_uint32() { - std::random_device dev; - std::mt19937 rng(dev()); - std::uniform_int_distribution dist(0, std::numeric_limits::max()); - return dist(rng); -} - bool random_bytes(uint8_t *data, size_t len) { FILE *fp = fopen("/dev/urandom", "r"); if (fp == nullptr) { diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index fce3d62dda..c0be270062 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -9,7 +9,7 @@ namespace esphome::host { namespace fs = std::filesystem; -static const char *const TAG = "host.preferences"; +static const char *const TAG = "preferences"; void HostPreferences::setup_() { if (this->setup_complete_) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 8bdea470b5..73dbda8694 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -487,12 +487,10 @@ template class HttpRequestSendAction : public Action { body = this->body_.value(x...); } if (!this->json_.empty()) { - auto f = std::bind(&HttpRequestSendAction::encode_json_, this, x..., std::placeholders::_1); - body = json::build_json(f); + body = json::build_json([this, x...](JsonObject root) { this->encode_json_(x..., root); }); } if (this->json_func_ != nullptr) { - auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); - body = json::build_json(f); + body = json::build_json([this, x...](JsonObject root) { this->json_func_(x..., root); }); } std::vector
request_headers; request_headers.reserve(this->request_headers_.size()); @@ -561,7 +559,6 @@ template class HttpRequestSendAction : public Action { root[item.first] = val.value(x...); } } - void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; FixedVector>> request_headers_{}; std::vector lower_case_collect_headers_{"content-type", "content-length"}; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 6ff75d7709..6fb0e46d93 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -28,6 +28,7 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, HexInt +from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) @@ -84,7 +85,7 @@ class ImageEncoder: def __init__(self, width, height, transparency, dither, invert_alpha): """ - :param width: The image width in pixels + :param width: The image width in pixels (or bytes) :param height: The image height in pixels :param transparency: Transparency type :param dither: Dither method @@ -93,11 +94,12 @@ class ImageEncoder: self.transparency = transparency self.width = width self.height = height - self.data = [0 for _ in range(width * height)] + self.data = [0] * width * height self.dither = dither self.index = 0 self.invert_alpha = invert_alpha self.path = "" + self.big_endian = False def convert(self, image, path): """ @@ -119,12 +121,21 @@ class ImageEncoder: :return: """ + def end_image(self): + """ + Called at the end of the image. + :return: + """ + + def set_big_endian(self, big_endian: bool) -> None: + self.big_endian = big_endian + @classmethod def is_endian(cls) -> bool: """ Check if the image encoder supports endianness configuration """ - return getattr(cls, "set_big_endian", None) is not None + return False @classmethod def get_options(cls) -> list[str]: @@ -212,18 +223,21 @@ class ImageGrayscale(ImageEncoder): class ImageRGB565(ImageEncoder): def __init__(self, width, height, transparency, dither, invert_alpha): - stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2 super().__init__( - width * stride, + width * 2, height, transparency, dither, invert_alpha, ) - self.big_endian = True + self.alpha = [0] * width * height - def set_big_endian(self, big_endian: bool) -> None: - self.big_endian = big_endian + @classmethod + def is_endian(cls) -> bool: + """ + Check if the image encoder supports endianness configuration + """ + return True def convert(self, image, path): return image.convert("RGBA") @@ -233,6 +247,9 @@ class ImageRGB565(ImageEncoder): r = r >> 3 g = g >> 2 b = b >> 3 + if self.invert_alpha: + a ^= 0xFF + self.alpha[self.index // 2] = a if self.transparency == CONF_CHROMA_KEY: if r == 0 and g == 1 and b == 0: g = 0 @@ -251,11 +268,10 @@ class ImageRGB565(ImageEncoder): self.index += 1 self.data[self.index] = rgb >> 8 self.index += 1 + + def end_image(self): if self.transparency == CONF_ALPHA_CHANNEL: - if self.invert_alpha: - a ^= 0xFF - self.data[self.index] = a - self.index += 1 + self.data.extend(self.alpha) class ImageRGB(ImageEncoder): @@ -281,11 +297,11 @@ class ImageRGB(ImageEncoder): r = 0 g = 1 b = 0 - self.data[self.index] = r + self.data[self.index] = b self.index += 1 self.data[self.index] = g self.index += 1 - self.data[self.index] = b + self.data[self.index] = r self.index += 1 if self.transparency == CONF_ALPHA_CHANNEL: if self.invert_alpha: @@ -655,6 +671,24 @@ def _config_schema(value): CONFIG_SCHEMA = _config_schema +def _final_validate(config): + """ + For LVGL 9 the default byte order for RGB565 images is little-endian + :param config: + :return: + """ + fv = full_config.get() + if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config): + config = config.copy() + for c in config: + if not c.get(CONF_BYTE_ORDER): + c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN" + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def write_image(config, all_frames=False): path = Path(config[CONF_FILE]) if not path.is_file(): @@ -720,6 +754,7 @@ async def write_image(config, all_frames=False): for col in range(width): encoder.encode(pixels[row * width + col]) encoder.end_row() + encoder.end_image() rhs = [HexInt(x) for x in encoder.data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) @@ -729,31 +764,24 @@ async def write_image(config, all_frames=False): return prog_arr, width, height, image_type, trans_value, frame_count -async def _image_to_code(entry): - """ - Convert a single image entry to code and return its metadata. - :param entry: The config entry for the image. - :return: An ImageMetaData object - """ - prog_arr, width, height, image_type, trans_value, _ = await write_image(entry) - cg.new_Pvariable(entry[CONF_ID], prog_arr, width, height, image_type, trans_value) - return ImageMetaData( - width, - height, - entry[CONF_TYPE], - entry[CONF_TRANSPARENCY], +def add_metadata(id: str, width: int, height: int, image_type: str, transparency): + all_metadata = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {}) + all_metadata[str(id)] = ImageMetaData( + width=width, height=height, image_type=image_type, transparency=transparency ) async def to_code(config): cg.add_define("USE_IMAGE") # By now the config will be a simple list. - # Use a subkey to allow for other data in the future - CORE.data[DOMAIN] = { - KEY_METADATA: { - entry[CONF_ID].id: await _image_to_code(entry) for entry in config - } - } + for entry in config: + prog_arr, width, height, image_type, trans_value, _ = await write_image(entry) + cg.new_Pvariable( + entry[CONF_ID], prog_arr, width, height, image_type, trans_value + ) + add_metadata( + entry[CONF_ID], width, height, entry[CONF_TYPE], entry[CONF_TRANSPARENCY] + ) def get_all_image_metadata() -> dict[str, ImageMetaData]: diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index 90e021467f..a6f9e35e2e 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -105,22 +105,22 @@ Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off } } #ifdef USE_LVGL -lv_img_dsc_t *Image::get_lv_img_dsc() { +lv_image_dsc_t *Image::get_lv_image_dsc() { // lazily construct lvgl image_dsc. if (this->dsc_.data != this->data_start_) { this->dsc_.data = this->data_start_; - this->dsc_.header.always_zero = 0; - this->dsc_.header.reserved = 0; + this->dsc_.header.reserved_2 = 0; + this->dsc_.header.stride = this->get_width_stride(); this->dsc_.header.w = this->width_; this->dsc_.header.h = this->height_; this->dsc_.data_size = this->get_width_stride() * this->get_height(); switch (this->get_type()) { case IMAGE_TYPE_BINARY: - this->dsc_.header.cf = LV_IMG_CF_ALPHA_1BIT; + this->dsc_.header.cf = LV_COLOR_FORMAT_A1; break; case IMAGE_TYPE_GRAYSCALE: - this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT; + this->dsc_.header.cf = LV_COLOR_FORMAT_A8; break; case IMAGE_TYPE_RGB: @@ -138,7 +138,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { } #else this->dsc_.header.cf = - this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888; + this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_COLOR_FORMAT_ARGB8888 : LV_COLOR_FORMAT_RGB888; #endif break; @@ -146,14 +146,10 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { #if LV_COLOR_DEPTH == 16 switch (this->transparency_) { case TRANSPARENCY_ALPHA_CHANNEL: - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; - break; - case TRANSPARENCY_CHROMA_KEY: - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED; + this->dsc_.header.cf = LV_COLOR_FORMAT_RGB565A8; break; default: - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; - break; + this->dsc_.header.cf = LV_COLOR_FORMAT_RGB565; } #else this->dsc_.header.cf = @@ -173,8 +169,8 @@ bool Image::get_binary_pixel_(int x, int y) const { } Color Image::get_rgb_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8; - Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2), 0xFF); + Color color = Color(progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 0), 0xFF); switch (this->transparency_) { case TRANSPARENCY_CHROMA_KEY: @@ -200,7 +196,7 @@ Color Image::get_rgb565_pixel_(int x, int y) const { auto a = 0xFF; switch (this->transparency_) { case TRANSPARENCY_ALPHA_CHANNEL: - a = progmem_read_byte(pos + 2); + a = progmem_read_byte(this->data_start_ + this->width_ * this->height_ * 2 + (x + y * this->width_)); break; case TRANSPARENCY_CHROMA_KEY: if (rgb565 == 0x0020) @@ -239,7 +235,7 @@ Image::Image(const uint8_t *data_start, int width, int height, ImageType type, T this->bpp_ = 8; break; case IMAGE_TYPE_RGB565: - this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16; + this->bpp_ = 16; break; case IMAGE_TYPE_RGB: this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24; diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index 4024ab1357..d4865570e4 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -41,7 +41,7 @@ class Image : public display::BaseImage { bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; } #ifdef USE_LVGL - lv_img_dsc_t *get_lv_img_dsc(); + lv_image_dsc_t *get_lv_image_dsc(); #endif protected: bool get_binary_pixel_(int x, int y) const; diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index edceb9a3b1..003328d535 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -245,8 +245,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); - this->set_timeout("wifi-connect-timeout", 30000, f); + this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); return true; } case improv::GET_CURRENT_STATE: diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 0a1147c924..6230a8c30b 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -530,10 +530,11 @@ void LD2450Component::handle_periodic_data_() { } #endif - // Store target info for zone target count - this->target_info_[index].x = tx; - this->target_info_[index].y = ty; - this->target_info_[index].is_moving = is_moving; + // Store target info for zone target count. Zero out untracked targets (td==0) + // so stale coordinates don't produce ghost counts in count_targets_in_zone_(). + this->target_info_[index].x = (td > 0) ? tx : 0; + this->target_info_[index].y = (td > 0) ? ty : 0; + this->target_info_[index].is_moving = (td > 0) && is_moving; } // End loop thru targets diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index d2f2d72acb..5b7b6c7ee6 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -193,7 +193,9 @@ void LEDCOutput::setup() { chan_conf.gpio_num = static_cast(this->pin_->get_pin()); chan_conf.speed_mode = speed_mode; chan_conf.channel = chan_num; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) chan_conf.intr_type = LEDC_INTR_DISABLE; +#endif chan_conf.timer_sel = timer_num; chan_conf.duty = this->inverted_ == this->pin_->is_inverted() ? 0 : (1U << this->bit_depth_); chan_conf.hpoint = hpoint; diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index ffbd181c54..52332ef53d 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -8,8 +8,6 @@ namespace esphome { -uint32_t random_uint32() { return rand(); } - bool random_bytes(uint8_t *data, size_t len) { lt_rand_bytes(data, len); return true; diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index f22c12f1fb..344ca4a8b3 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -9,7 +9,7 @@ namespace esphome::libretiny { -static const char *const TAG = "lt.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 4403281116..64452e4282 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -81,18 +81,32 @@ def _get_data() -> LightData: return CORE.data[DOMAIN] +def generate_gamma_table(gamma_correct: float) -> list[HexInt]: + """Generate a 256-entry uint16 gamma lookup table. + + For gamma > 0, non-zero indices are clamped to a minimum of 1 to preserve + the invariant that non-zero input always produces non-zero output. Without + this, small brightness values (e.g. 1%) get quantized to exactly 0.0, + which breaks zero_means_zero logic in FloatOutput. + """ + if gamma_correct > 0: + return [ + HexInt( + max(1, min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) + if i > 0 + else HexInt(0) + ) + for i in range(256) + ] + return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + + def _get_or_create_gamma_table(gamma_correct): data = _get_data() if gamma_correct in data.gamma_tables: return data.gamma_tables[gamma_correct] - if gamma_correct > 0: - forward = [ - HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) - for i in range(256) - ] - else: - forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + forward = generate_gamma_table(gamma_correct) gamma_str = f"{gamma_correct}".replace(".", "_") fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 3a9ca8c8c2..a2c2dbca46 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -154,6 +154,16 @@ class LightColorValues { } /// Convert these light color values to an CWWW representation with the given parameters. + /// + /// Note on gamma and constant_brightness: This method operates on the raw/internal channel + /// values stored in this object. For cold_white_ and warm_white_ specifically, these + /// may already be gamma-uncorrected when derived from a color_temperature value. + /// For constant_brightness=false, additional gamma for the output can be applied after + /// this method since gamma commutes with simple multiplication. For constant_brightness=true, + /// the caller (LightState::current_values_as_cwww) must apply gamma to the individual + /// channel values BEFORE the balancing formula, because the nonlinear max/sum ratio does + /// not commute with gamma. See LightState::current_values_as_cwww() for the correct + /// implementation. void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const { if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { const float cw_level = this->cold_white_; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 161092532a..1b736d84f6 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -223,12 +223,11 @@ void LightState::current_values_as_rgbw(float *red, float *green, float *blue, f } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness); + this->current_values.as_rgb(red, green, blue); *red = this->gamma_correct_lut(*red); *green = this->gamma_correct_lut(*green); *blue = this->gamma_correct_lut(*blue); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + this->current_values_as_cwww(cold_white, warm_white, constant_brightness); } void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness) { @@ -241,9 +240,45 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue, *white_brightness = this->gamma_correct_lut(*white_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_cwww(cold_white, warm_white, constant_brightness); - *cold_white = this->gamma_correct_lut(*cold_white); - *warm_white = this->gamma_correct_lut(*warm_white); + if (!constant_brightness) { + // Without constant_brightness, gamma commutes with simple multiplication: + // gamma(white_level * cw) = gamma(white_level) * gamma(cw) + // (since gamma(a*b) = (a*b)^g = a^g * b^g = gamma(a) * gamma(b)) + // so applying gamma after is mathematically equivalent and simpler. + this->current_values.as_cwww(cold_white, warm_white, false); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); + return; + } + + // For constant_brightness mode, gamma MUST be applied to the individual + // channel values BEFORE the balancing formula (max/sum ratio), not after. + // + // Why: The cold_white_ and warm_white_ values stored in LightColorValues + // are gamma-uncorrected (see transform_parameters_() which applies + // gamma_uncorrect to the linear CW/WW fractions derived from color + // temperature). Applying gamma_correct here recovers the original linear + // fractions, which the constant_brightness formula then uses to distribute + // power evenly. The max/sum formula ensures cold+warm PWM output sums to + // a constant, keeping total power (and perceived brightness) the same + // across all color temperatures. + // + // Applying gamma AFTER the formula would be incorrect because gamma is + // nonlinear: gamma(a/b) != gamma(a)/gamma(b), so the carefully balanced + // ratio would be distorted, causing a severe brightness dip at mid-range + // color temperatures. + const auto &v = this->current_values; + if (!(v.get_color_mode() & ColorCapability::COLD_WARM_WHITE)) { + *cold_white = *warm_white = 0; + return; + } + + const float cw_level = this->gamma_correct_lut(v.get_cold_white()); + const float ww_level = this->gamma_correct_lut(v.get_warm_white()); + const float white_level = this->gamma_correct_lut(v.get_state() * v.get_brightness()); + const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero. + *cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum; + *warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum; } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 011c6cc6af..6f3c422693 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -51,13 +51,16 @@ template class LockCondition : public Condition { template class LockStateTrigger : public Trigger<> { public: - explicit LockStateTrigger(Lock *a_lock) { - a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == State) { + explicit LockStateTrigger(Lock *a_lock) : lock_(a_lock) { + a_lock->add_on_state_callback([this]() { + if (this->lock_->state == State) { this->trigger(); } }); } + + protected: + Lock *lock_; }; using LockLockTrigger = LockStateTrigger; diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 675f9a2ca4..4345e291a3 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -56,6 +56,7 @@ from esphome.const import ( PlatformFramework, ) from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority +from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -323,19 +324,34 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(CoroPriority.DIAGNOSTICS) -async def to_code(config): - baud_rate = config[CONF_BAUD_RATE] +@coroutine_with_priority(CoroPriority.EARLY_INIT) +async def to_code(config: ConfigType) -> None: + baud_rate: int = config[CONF_BAUD_RATE] level = config[CONF_LEVEL] CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level - initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] tx_buffer_size = config[CONF_TX_BUFFER_SIZE] cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size) - log = cg.new_Pvariable( - config[CONF_ID], - baud_rate, - ) - if CORE.is_esp32: + # Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early + # so the constructor can allocate the buffer immediately, preventing a race + # where another task logs before the buffer is initialized. + task_log_buffer_size = 0 + if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: + task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] + elif CORE.is_host: + task_log_buffer_size = 64 # Fixed 64 slots for host + if task_log_buffer_size > 0: + cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + task_log_buffer_size, + ) + else: + log = cg.new_Pvariable( + config[CONF_ID], + baud_rate, + ) + if CORE.is_esp32 or CORE.is_host: cg.add(log.create_pthread_key()) # set_uart_selection() must be called before pre_setup() because # pre_setup() switches on uart_ to decide which hardware to initialize @@ -347,24 +363,28 @@ async def to_code(config): HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]] ) ) - # pre_setup() must be called before init_log_buffer() because - # init_log_buffer() calls disable_loop() which may log at VV level, - # and global_logger must be set before any logging occurs. + # pre_setup() sets global_logger and must run before any other code + # that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV). cg.add(log.pre_setup()) - if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52: - task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] - if task_log_buffer_size > 0: - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(task_log_buffer_size)) - if CORE.using_zephyr: - zephyr_add_prj_conf("MPSC_PBUF", True) - elif CORE.is_host: - cg.add(log.create_pthread_key()) - cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") - cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host - + initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] cg.add(log.set_log_level(initial_level)) + # Schedule the rest of logger setup at DIAGNOSTICS priority, after + # Application is constructed (CORE priority) but before most components. + CORE.add_job(_late_logger_init, config) + + +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) +async def _late_logger_init(config: ConfigType) -> None: + """Finish logger setup after Application is constructed.""" + log = await cg.get_variable(config[CONF_ID]) + level = config[CONF_LEVEL] + baud_rate: int = config[CONF_BAUD_RATE] + if CORE.using_zephyr: + task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0) + if task_log_buffer_size > 0: + zephyr_add_prj_conf("MPSC_PBUF", True) + # Enable runtime tag levels if logs are configured or explicitly enabled logs_config = config[CONF_LOGS] if logs_config or config[CONF_RUNTIME_TAG_LEVELS]: @@ -594,6 +614,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + "task_log_buffer_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/logger/log_buffer.h b/esphome/components/logger/log_buffer.h index 734cb14dc5..067ce04114 100644 --- a/esphome/components/logger/log_buffer.h +++ b/esphome/components/logger/log_buffer.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -8,8 +9,8 @@ namespace esphome::logger { // Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin) static constexpr uint16_t MAX_HEADER_SIZE = 128; -// ANSI color code last digit (30-38 range, store only last digit to save RAM) -static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { +// ANSI color code last digit (30-38 range, store only last digit to save RAM on ESP8266) +static const char LOG_LEVEL_COLOR_DIGIT[] PROGMEM = { '\0', // NONE '1', // ERROR (31 = red) '3', // WARNING (33 = yellow) @@ -20,7 +21,7 @@ static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { '8', // VERY_VERBOSE (38 = white) }; -static constexpr char LOG_LEVEL_LETTER_CHARS[] = { +static const char LOG_LEVEL_LETTER_CHARS[] PROGMEM = { '\0', // NONE 'E', // ERROR 'W', // WARNING @@ -64,7 +65,7 @@ struct LogBuffer { *p++ = 'V'; // VERY_VERBOSE = "VV" *p++ = 'V'; } else { - *p++ = LOG_LEVEL_LETTER_CHARS[level]; + *p++ = static_cast(progmem_read_byte(reinterpret_cast(&LOG_LEVEL_LETTER_CHARS[level]))); } } *p++ = ']'; @@ -184,7 +185,7 @@ struct LogBuffer { *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold *p++ = ';'; *p++ = '3'; - *p++ = LOG_LEVEL_COLOR_DIGIT[level]; + *p++ = static_cast(progmem_read_byte(reinterpret_cast(&LOG_LEVEL_COLOR_DIGIT[level]))); *p++ = 'm'; } // Copy string without null terminator, updates pointer in place diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 497809cd2e..ceacded775 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -152,29 +152,25 @@ inline uint8_t Logger::level_for(const char *tag) { return this->current_level_; } +#ifdef USE_ESPHOME_TASK_LOG_BUFFER +Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) { +#else Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) { +#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); #elif defined(USE_HOST) - this->main_thread_ = pthread_self(); +this->main_thread_ = pthread_self(); #endif -} #ifdef USE_ESPHOME_TASK_LOG_BUFFER -void Logger::init_log_buffer(size_t total_buffer_size) { - // Host uses slot count instead of byte size // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed - this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size); - -#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) - // Start with loop disabled when using task buffer - // The loop will be enabled automatically when messages arrive - // Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_() - this->disable_loop_when_buffer_empty_(); + this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size); + // Note: we don't disable loop here because the component isn't registered with App yet. + // The loop self-disables on its first iteration when it finds no messages to process. #endif } -#endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void Logger::loop() { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8c38cadcbc..c81b8e4e94 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -143,9 +143,10 @@ enum UARTSelection : uint8_t { */ class Logger final : public Component { public: - explicit Logger(uint32_t baud_rate); #ifdef USE_ESPHOME_TASK_LOG_BUFFER - void init_log_buffer(size_t total_buffer_size); + explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size); +#else + explicit Logger(uint32_t baud_rate); #endif #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)) void loop() override; diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c9cad1ac90..c37a32ecca 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,12 +1,30 @@ import importlib -import logging from pathlib import Path import pkgutil from esphome.automation import build_automation, validate_automation import esphome.codegen as cg -from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING +from esphome.components.const import ( + CONF_BYTE_ORDER, + CONF_COLOR_DEPTH, + CONF_DRAW_ROUNDING, +) from esphome.components.display import Display +from esphome.components.esp32 import ( + VARIANT_ESP32P4, + add_idf_component, + add_idf_sdkconfig_option, + get_esp32_variant, +) +from esphome.components.image import ( + CONF_OPAQUE, + IMAGE_TYPE, + ImageBinary, + ImageGrayscale, + ImageRGB, + ImageRGB565, + get_image_metadata, +) from esphome.components.psram import DOMAIN as PSRAM_DOMAIN import esphome.config_validation as cv from esphome.const import ( @@ -21,7 +39,6 @@ from esphome.const import ( CONF_PAGES, CONF_TIMEOUT, CONF_TRIGGER_ID, - CONF_TYPE, ) from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj @@ -30,8 +47,7 @@ from esphome.helpers import write_file_if_changed from esphome.yaml_util import load_yaml from . import defines as df, helpers, lv_validation as lvalid, widgets -from .automation import disp_update, focused_widgets, refreshed_widgets -from .defines import add_define +from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets from .encoders import ( ENCODERS_CONFIG, encoders_to_code, @@ -45,12 +61,13 @@ from .lvcode import LvContext, LvglComponent, lvgl_static from .schemas import ( DISP_BG_SCHEMA, FULL_STYLE_SCHEMA, + STYLE_REMAP, WIDGET_TYPES, any_widget_schema, container_schema, obj_schema, ) -from .styles import add_top_layer, styles_to_code, theme_to_code +from .styles import styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code from .trigger import add_on_boot_triggers, generate_triggers from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns @@ -58,7 +75,7 @@ from .widgets import ( LvScrActType, Widget, add_widgets, - get_scr_act, + get_screen_active, set_obj_properties, styles_used, ) @@ -84,7 +101,6 @@ DOMAIN = "lvgl" DEPENDENCIES = ["display"] AUTO_LOAD = ["key_provider"] CODEOWNERS = ["@clydebarrow"] -LOGGER = logging.getLogger(__name__) HELLO_WORLD_FILE = "hello_world.yaml" @@ -102,6 +118,7 @@ def as_macro(macro, value): return f"#define {macro} {value}" +LVGL_VERSION = "9.5.0" LV_CONF_FILENAME = "lv_conf.h" LV_CONF_H_FORMAT = """\ #pragma once @@ -110,7 +127,17 @@ LV_CONF_H_FORMAT = """\ def generate_lv_conf_h(): - definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()] + # Get all possible LV_ config defines based on the widgets used in the config, and the standard LVGL options + all_defines = set( + df.LV_DEFINES + tuple(f"LV_USE_{w.upper()}" for w in WIDGET_TYPES) + ) + # Get the defines that are actually used based on the config + lv_defines = df.get_data(df.KEY_LV_DEFINES) + unused_defines = all_defines - set(lv_defines) + # Create the content of lv_conf.h with the used defines set to their value, and the unused defines disabled + definitions = [as_macro(m, v) for m, v in lv_defines.items()] + [ + as_macro(m, "0") for m in unused_defines + ] definitions.sort() return LV_CONF_H_FORMAT.format("\n".join(definitions)) @@ -133,7 +160,7 @@ def multi_conf_validate(configs: list[dict]): for item in ( CONF_LOG_LEVEL, CONF_COLOR_DEPTH, - df.CONF_BYTE_ORDER, + CONF_BYTE_ORDER, df.CONF_TRANSPARENCY_KEY, ): if base_config[item] != config[item]: @@ -166,14 +193,7 @@ def final_validation(config_list): ) buffer_frac = config[CONF_BUFFER_SIZE] if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config: - LOGGER.warning("buffer_size: may need to be reduced without PSRAM") - for image_id in lv_images_used: - path = global_config.get_path_for_id(image_id)[:-1] - image_conf = global_config.get_config_for_path(path) - if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): - raise cv.Invalid( - "Using RGBA or RGB24 in image config not compatible with LVGL", path - ) + df.LOGGER.warning("buffer_size: may need to be reduced without PSRAM") for w in focused_widgets: path = global_config.get_path_for_id(w) widget_conf = global_config.get_config_for_path(path[:-1]) @@ -205,39 +225,48 @@ def final_validation(config_list): async def to_code(configs): config_0 = configs[0] # Global configuration - cg.add_library("lvgl/lvgl", "8.4.0") + if CORE.is_esp32: + if get_esp32_variant() == VARIANT_ESP32P4: + add_idf_sdkconfig_option("CONFIG_LV_DRAW_BUF_ALIGN", 64) + # disable use of PPA for fills until upstream bugs fixed + df.add_define("LV_USE_PPA", "0") + df.add_define("LV_DRAW_BUF_ALIGN", "64") + else: + df.add_define("LV_DRAW_BUF_ALIGN", "32") + add_idf_component(name="lvgl/lvgl", ref=LVGL_VERSION) + else: + df.add_define("LV_DRAW_BUF_ALIGN", "1") + cg.add_library("lvgl/lvgl", LVGL_VERSION) + df.add_define("LV_DRAW_BUF_STRIDE_ALIGN", "1") + df.add_define("LV_USE_DRAW_SW", "1") + df.add_define("LV_USE_STDLIB_SPRINTF", "LV_STDLIB_CLIB") + df.add_define("LV_USE_STDLIB_STRING", "LV_STDLIB_CLIB") + df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM") cg.add_define("USE_LVGL") # suppress default enabling of extra widgets - add_define("_LV_KCONFIG_PRESENT") + # cg.add_define("LV_KCONFIG_PRESENT") # Always enable - lots of things use it. - add_define("LV_DRAW_COMPLEX", "1") - add_define("LV_TICK_CUSTOM", "1") - add_define("LV_TICK_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') - add_define("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(lv_millis())") - add_define("LV_MEM_CUSTOM", "1") - add_define("LV_MEM_CUSTOM_ALLOC", "lv_custom_mem_alloc") - add_define("LV_MEM_CUSTOM_FREE", "lv_custom_mem_free") - add_define("LV_MEM_CUSTOM_REALLOC", "lv_custom_mem_realloc") - add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') + df.add_define("LV_DRAW_SW_COMPLEX", "1") - add_define( + df.add_define( "LV_LOG_LEVEL", f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[CONF_LOG_LEVEL]]}", ) + df.add_define("LV_USE_LOG", "1") cg.add_define( "LVGL_LOG_LEVEL", cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"), ) - add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) + df.add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) for font in helpers.lv_fonts_used: - add_define(f"LV_FONT_{font.upper()}") + df.add_define(f"LV_FONT_{font.upper()}") if config_0[CONF_COLOR_DEPTH] == 16: - add_define( + df.add_define( "LV_COLOR_16_SWAP", - "1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0", + "1" if config_0[CONF_BYTE_ORDER] == "big_endian" else "0", ) - add_define( + df.add_define( "LV_COLOR_CHROMA_KEY", await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]), ) @@ -248,7 +277,7 @@ async def to_code(configs): await cg.get_variable(font) default_font = config_0[df.CONF_DEFAULT_FONT] if not lvalid.is_lv_font(default_font): - add_define( + df.add_define( "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})" ) globfont_id = ID( @@ -262,9 +291,9 @@ async def to_code(configs): MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(), static=False, ) - add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) + df.add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: - add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) + df.add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) cg.add(lvgl_static.esphome_lvgl_init()) default_group = get_default_group(config_0) @@ -293,8 +322,9 @@ async def to_code(configs): await cg.register_component(lv_component, config) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) - lv_scr_act = get_scr_act(lv_component) + lv_scr_act = get_screen_active(lv_component) async with LvContext(): + cg.add(lv_component.set_big_endian(config[CONF_BYTE_ORDER] == "big_endian")) await touchscreens_to_code(lv_component, config) await encoders_to_code(lv_component, config, default_group) await keypads_to_code(lv_component, config, default_group) @@ -304,9 +334,10 @@ async def to_code(configs): await set_obj_properties(lv_scr_act, config) await add_widgets(lv_scr_act, config) await add_pages(lv_component, config) - await add_top_layer(lv_component, config) + await layers_to_code(lv_component, config) + await lvgl_update(lv_component, config) await msgboxes_to_code(lv_component, config) - await disp_update(lv_component.get_disp(), config) + # await disp_update(lv_component.get_disp(), config) # Set this directly since we are limited in how many methods can be added to the Widget class. Widget.widgets_completed = True async with LvContext(): @@ -336,15 +367,53 @@ async def to_code(configs): # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - if {"transform_angle", "transform_zoom"} & styles_used: - add_define("LV_COLOR_SCREEN_TRANSP", "1") + lv_image_formats = df.get_color_formats().copy() + if { + "transform_rotation", + "transform_scale", + "transform_scale_x", + "transform_scale_y", + } & styles_used: + df.add_define("LV_COLOR_SCREEN_TRANSP", "1") + lv_image_formats.add("ARGB8888") + lv_image_formats.add( + "RGB565" + ) # Currently always need RGB565 for the display buffer for use in helpers.lv_uses: - add_define(f"LV_USE_{use.upper()}") + df.add_define(f"LV_USE_{use.upper()}") cg.add_define(f"USE_LVGL_{use.upper()}") + + for image_id in lv_images_used: + await cg.get_variable(image_id) + metadata = get_image_metadata(image_id.id) + image_type = IMAGE_TYPE[metadata.image_type] + transparent = metadata.transparency != CONF_OPAQUE + if transparent: + # Internal draw layer will use ARGB8888 + lv_image_formats.add("ARGB8888") + if image_type == ImageBinary: + lv_image_formats.add("I1") + if image_type == ImageGrayscale: + lv_image_formats.add("A8") + if image_type == ImageRGB565: + lv_image_formats.add("RGB565A8" if transparent else "RGB565") + if image_type == ImageRGB: + lv_image_formats.add("ARGB8888" if transparent else "RGB8888") + if df.is_defined("LV_GRADIENT_MAX_STOPS"): + lv_image_formats.add("RGB888") + for fmt in lv_image_formats: + df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1") lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) cg.add_build_flag("-DLV_CONF_H=1") - cg.add_build_flag(f'-DLV_CONF_PATH="{LV_CONF_FILENAME}"') + cg.add_build_flag(f'-DLV_CONF_PATH=\\"{LV_CONF_FILENAME}\\"') + + for prop in df.get_remapped_uses(): + df.LOGGER.warning( + "Property '%s' is deprecated, use '%s' instead", prop, STYLE_REMAP[prop] + ) + for warning in df.get_warnings(): + df.LOGGER.warning(warning) def display_schema(config): @@ -357,7 +426,9 @@ def display_schema(config): def add_hello_world(config): if df.CONF_WIDGETS not in config and CONF_PAGES not in config: - LOGGER.info("No pages or widgets configured, creating default hello_world page") + df.LOGGER.info( + "No pages or widgets configured, creating default hello_world page" + ) hello_world_path = Path(__file__).parent / HELLO_WORLD_FILE config[df.CONF_WIDGETS] = any_widget_schema()(load_yaml(hello_world_path)) return config @@ -395,8 +466,8 @@ LVGL_SCHEMA = cv.All( cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), - cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( - "big_endian", "little_endian" + cv.Optional(CONF_BYTE_ORDER, default="big_endian"): cv.one_of( + "big_endian", "little_endian", lower=True ), cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list( cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}).extend( @@ -424,6 +495,7 @@ LVGL_SCHEMA = cv.All( cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), + cv.Optional(df.CONF_BOTTOM_LAYER): container_schema(obj_spec), cv.Optional( df.CONF_TRANSPARENCY_KEY, default=0x000400 ): lvalid.lv_color, diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index f9adca9c33..24579e5be8 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -10,18 +10,21 @@ from esphome.cpp_generator import TemplateArguments, get_variable from esphome.cpp_types import nullptr from .defines import ( - CONF_DISP_BG_COLOR, - CONF_DISP_BG_IMAGE, - CONF_DISP_BG_OPA, + CONF_BG_OPA, + CONF_BOTTOM_LAYER, CONF_EDITING, CONF_FREEZE, CONF_LVGL_ID, + CONF_MAIN, + CONF_OBJ, + CONF_SCROLLBAR, CONF_SHOW_SNOW, + CONF_TOP_LAYER, PARTS, - literal, - static_cast, + StaticCastExpression, + add_warning, ) -from .lv_validation import lv_bool, lv_color, lv_image, lv_milliseconds, opacity +from .lv_validation import lv_bool, lv_milliseconds from .lvcode import ( LVGL_COMP_ARG, UPDATE_EVENT, @@ -42,13 +45,13 @@ from .schemas import ( LIST_ACTION_SCHEMA, LVGL_SCHEMA, base_update_schema, + part_schema, ) from .types import ( LV_STATE, LvglAction, LvglCondition, ObjUpdateAction, - lv_disp_t, lv_group_t, lv_obj_base_t, lv_obj_t, @@ -56,7 +59,9 @@ from .types import ( ) from .widgets import ( Widget, - get_scr_act, + WidgetType, + add_widgets, + get_screen_active, get_widgets, set_obj_properties, wait_for_widgets, @@ -67,6 +72,41 @@ focused_widgets = set() refreshed_widgets = set() +async def layers_to_code(lv_component, config): + if top_conf := config.get(CONF_TOP_LAYER): + top_layer = lv_expr.display_get_layer_top(lv_component.get_disp()) + with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj: + top_w = Widget(top_layer_obj, layer_spec, top_conf) + await set_obj_properties(top_w, top_conf) + await add_widgets(top_w, top_conf) + if bottom_conf := config.get(CONF_BOTTOM_LAYER): + bottom_layer = lv_expr.display_get_layer_bottom(lv_component.get_disp()) + with LocalVariable("bottom_layer", lv_obj_t, bottom_layer) as bottom_layer_obj: + bottom_w = Widget(bottom_layer_obj, layer_spec, bottom_conf) + await set_obj_properties(bottom_w, bottom_conf) + await add_widgets(bottom_w, bottom_conf) + + +async def lvgl_update(lv_component, config): + bottom = {k.removeprefix("disp_"): v for k, v in config.items() if k in DISP_PROPS} + if not bottom: + return + plural = len(bottom) != 1 + add_warning( + "The propert" + + ("ies " if plural else "y ") + + "'" + + "','".join(k for k in config if k in DISP_PROPS) + + "'" + + (" are " if plural else " is ") + + "deprecated, use 'bottom_layer' instead." + ) + # Preserve default opacity from 8.x + if CONF_BG_OPA not in bottom: + bottom[CONF_BG_OPA] = 1.0 + await layers_to_code(lv_component, {CONF_BOTTOM_LAYER: bottom}) + + async def action_to_code( widgets: list[Widget], action: Callable[[Widget], Any], @@ -151,25 +191,6 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): return var -async def disp_update(disp, config: dict): - if ( - CONF_DISP_BG_COLOR not in config - and CONF_DISP_BG_IMAGE not in config - and CONF_DISP_BG_OPA not in config - ): - return - with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp: - if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: - lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) - if bg_image := config.get(CONF_DISP_BG_IMAGE): - if bg_image == "none": - lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr")) - else: - lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) - if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None: - lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa)) - - @automation.register_action( "lvgl.widget.redraw", ObjUpdateAction, @@ -187,7 +208,7 @@ async def disp_update(disp, config: dict): async def obj_invalidate_to_code(config, action_id, template_arg, args): if CONF_LVGL_ID in config: lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) - widgets = [get_scr_act(lv_comp)] + widgets = [get_screen_active(lv_comp)] else: widgets = await get_widgets(config) @@ -197,20 +218,30 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args): return await action_to_code(widgets, do_invalidate, action_id, template_arg, args) +layer_spec = WidgetType(CONF_OBJ, lv_obj_t, (CONF_MAIN, CONF_SCROLLBAR), is_mock=True) + +DISP_PROPS = {str(x) for x in DISP_BG_SCHEMA.schema} + + @automation.register_action( "lvgl.update", LvglAction, - DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra( - cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE) + part_schema(layer_spec.parts) + .extend(LVGL_SCHEMA) + .extend(DISP_BG_SCHEMA) + .extend( + { + cv.Optional(CONF_TOP_LAYER): part_schema(layer_spec.parts), + cv.Optional(CONF_BOTTOM_LAYER): part_schema(layer_spec.parts), + } ), synchronous=True, ) async def lvgl_update_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config, CONF_LVGL_ID) w = widgets[0] - disp = literal(f"{w.obj}->get_disp()") async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: - await disp_update(disp, config) + await lvgl_update(w.var, config) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) await cg.register_parented(var, w.var) return var @@ -336,7 +367,7 @@ async def widget_focus(config, action_id, template_arg, args): widget = await get_widgets(config) if widget: widget = widget[0] - group = static_cast( + group = StaticCastExpression( lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj) ) elif group := config.get(CONF_GROUP): diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 91077a1ff4..0a53d88669 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -10,8 +10,12 @@ from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS from esphome.core import CORE, ID, Lambda -from esphome.cpp_generator import LambdaExpression, MockObj -from esphome.cpp_types import uint32 +from esphome.cpp_generator import ( + CallExpression, + LambdaExpression, + MockObj, + MockObjClass, +) from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType @@ -21,8 +25,11 @@ LOGGER = logging.getLogger(__name__) lvgl_ns = cg.esphome_ns.namespace("lvgl") DOMAIN = "lvgl" +KEY_COLOR_FORMATS = "color_formats" KEY_LV_DEFINES = "lv_defines" +KEY_REMAPPED_USES = "remapped_uses" KEY_UPDATED_WIDGETS = "updated_widgets" +KEY_WARNINGS = "warnings" def get_data(key, default=None): @@ -33,10 +40,37 @@ def get_data(key, default=None): :return: """ return CORE.data.setdefault(DOMAIN, {}).setdefault( - key, default if default is not None else {} + key, {} if default is None else default ) +def get_warnings(): + return get_data(KEY_WARNINGS, set()) + + +def get_remapped_uses(): + return get_data(KEY_REMAPPED_USES, set()) + + +def get_color_formats(): + return get_data(KEY_COLOR_FORMATS, set()) + + +def add_warning(msg: str): + get_warnings().add(msg) + + +class StaticCastExpression(Expression): + __slots__ = ("type", "exp") + + def __init__(self, type: Any, exp: SafeExpType): + self.type = str(type) + self.exp = cg.safe_exp(exp) + + def __str__(self): + return f"static_cast<{self.type}>({self.exp})" + + def add_define(macro, value="1"): lv_defines = get_data(KEY_LV_DEFINES) value = str(value) @@ -47,27 +81,43 @@ def add_define(macro, value="1"): lv_defines[macro] = value +def is_defined(macro): + return macro in get_data(KEY_LV_DEFINES) + + def literal(arg) -> MockObj: if isinstance(arg, str): return MockObj(arg) return arg -def static_cast(type, value): - return literal(f"static_cast<{type}>({value})") +def addr(arg) -> MockObj: + return MockObj(f"&{arg}") def call_lambda(lamb: LambdaExpression): + """ + Given a lambda, either reduce to a simple expression or call it, possibly with parameters + from the surrounding context + :param lamb: + :return: + """ expr = lamb.content.strip() if expr.startswith("return") and expr.endswith(";"): - return expr[6:-1].strip() - # If lambda has parameters, call it with those parameter names + # Convert a lambda returning a simple expression to just that expression + expr = cg.RawExpression(expr[6:-1].strip()) + # Don't cast if the return type is a class + if isinstance(lamb.return_type, MockObjClass): + return expr + return StaticCastExpression(lamb.return_type, expr) + # If lambda has parameters, call it with their names # Parameter names come from hardcoded component code (like "x", "it", "event") # not from user input, so they're safe to use directly if lamb.parameters and lamb.parameters.parameters: - param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters) - return f"{lamb}({param_names})" - return f"{lamb}()" + return CallExpression( + lamb, *[MockObj(x.id) for x in lamb.parameters.parameters] + ) + return CallExpression(lamb) class LValidator: @@ -76,7 +126,7 @@ class LValidator: has `process()` to convert a value during code generation """ - def __init__(self, validator, rtype, retmapper=None, requires=None): + def __init__(self, validator, rtype: MockObj, retmapper=None, requires=None): self.validator = validator self.rtype = rtype self.retmapper = retmapper @@ -99,10 +149,9 @@ class LValidator: from .lvcode import get_lambda_context_args args = args or get_lambda_context_args() - return cg.RawExpression( - call_lambda( - await cg.process_lambda(value, args, return_type=self.rtype) - ) + + return call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) ) if self.retmapper is not None: return self.retmapper(value) @@ -112,6 +161,8 @@ class LValidator: value = [ await cg.get_variable(x) if isinstance(x, ID) else x for x in value ] + if self.rtype is cg.int_: + value = int(value) return cg.safe_exp(value) @@ -122,10 +173,11 @@ class LvConstant(LValidator): The property `one_of` has the single case validator, and `several_of` allows a list of constants. """ - def __init__(self, prefix: str, *choices): + def __init__(self, prefix: str, *choices, typename=None): self.prefix = prefix - self.choices = choices - prefixed_choices = [prefix + v for v in choices] + self.choices = tuple(x.upper() for x in choices) + self.typename = typename or prefix.lower() + "t" + prefixed_choices = [prefix + v.upper() for v in choices] prefixed_validator = cv.one_of(*prefixed_choices, upper=True) @schema_extractor("one_of") @@ -136,24 +188,30 @@ class LvConstant(LValidator): return prefixed_validator(value) return self.prefix + cv.one_of(*choices, upper=True)(value) - super().__init__(validator, rtype=uint32) + super().__init__(validator, rtype=cg.uint32) self.retmapper = self.mapper - self.one_of = LValidator(validator, uint32, retmapper=self.mapper) + self.one_of = LValidator(validator, cg.uint32, retmapper=self.mapper) self.several_of = LValidator( - cv.ensure_list(self.one_of), uint32, retmapper=self.mapper + cv.ensure_list(self.one_of), cg.uint32, retmapper=self.mapper ) def mapper(self, value): if not isinstance(value, list): value = [value] - return literal( - "|".join( - [ - str(v) if str(v).startswith(self.prefix) else self.prefix + str(v) - for v in value - ] - ).upper() - ) + value = [ + ( + str(v).upper() + if str(v).startswith(self.prefix) + else self.prefix + str(v).upper() + ) + for v in value + ] + if len(value) == 1: + return literal(value[0]) + value = literal("|".join(value)) + if self.typename is None: + return value + return StaticCastExpression(self.typename, value) def extend(self, *choices): """ @@ -161,7 +219,14 @@ class LvConstant(LValidator): :param choices: The extra choices :return: A new LVConstant instance """ - return LvConstant(self.prefix, *(self.choices + choices)) + return LvConstant( + self.prefix, *(self.choices + choices), typename=self.typename + ) + + def __getattr__(self, item): + if item.upper() not in self.choices: + raise AttributeError(f"{item} not one of {self.choices}") + return self.mapper(item) # Parts @@ -215,7 +280,7 @@ SWIPE_TRIGGERS = tuple( LV_ANIM = LvConstant( - "LV_SCR_LOAD_ANIM_", + "LV_SCREEN_LOAD_ANIM_", "NONE", "OVER_LEFT", "OVER_RIGHT", @@ -277,11 +342,13 @@ PARTS = ( CONF_KNOB, CONF_SELECTED, CONF_ITEMS, - CONF_TICKS, + # CONF_TICKS, CONF_CURSOR, CONF_TEXTAREA_PLACEHOLDER, ) +LV_PART = LvConstant("LV_PART_", *(p.upper() for p in PARTS)) + KEYBOARD_MODES = LvConstant( "LV_KEYBOARD_MODE_", "TEXT_LOWER", @@ -359,6 +426,7 @@ OBJ_FLAGS = ( "overflow_visible", "layout_1", "layout_2", + "send_draw_task_events", "widget_1", "widget_2", "user_1", @@ -366,12 +434,14 @@ OBJ_FLAGS = ( "user_3", "user_4", ) +LV_OBJ_FLAG = LvConstant("LV_OBJ_FLAG_", *OBJ_FLAGS) ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") +SLIDER_MODES = LvConstant("LV_SLIDER_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") BUTTONMATRIX_CTRLS = LvConstant( - "LV_BTNMATRIX_CTRL_", + "LV_BUTTONMATRIX_CTRL_", "HIDDEN", "NO_REPEAT", "DISABLED", @@ -434,12 +504,16 @@ CONF_ACCEPTED_CHARS = "accepted_chars" CONF_ADJUSTABLE = "adjustable" CONF_ALIGN = "align" CONF_ALIGN_TO = "align_to" +CONF_ANGLE_RANGE = "angle_range" CONF_ANIMATED = "animated" CONF_ANIMATION = "animation" +CONF_ANIMATIONS = "animations" CONF_ANTIALIAS = "antialias" CONF_ARC_LENGTH = "arc_length" CONF_AUTO_START = "auto_start" CONF_BACKGROUND_STYLE = "background_style" +CONF_BG_OPA = "bg_opa" +CONF_BOTTOM_LAYER = "bottom_layer" CONF_BUTTON_STYLE = "button_style" CONF_DECIMAL_PLACES = "decimal_places" CONF_COLUMN = "column" @@ -449,9 +523,11 @@ CONF_DISP_BG_IMAGE = "disp_bg_image" CONF_DISP_BG_OPA = "disp_bg_opa" CONF_BODY = "body" CONF_BUTTONS = "buttons" -CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" +CONF_COLOR_DEPTH = "color_depth" +CONF_COLOR_END = "color_end" +CONF_COLOR_START = "color_start" CONF_CONTAINER = "container" CONF_CONTROL = "control" CONF_DEFAULT_FONT = "default_font" @@ -483,8 +559,10 @@ CONF_GRID_COLUMN_ALIGN = "grid_column_align" CONF_GRID_COLUMNS = "grid_columns" CONF_GRID_ROW_ALIGN = "grid_row_align" CONF_GRID_ROWS = "grid_rows" +CONF_HEADER_BUTTONS = "header_buttons" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" +CONF_INDICATORS = "indicators" CONF_INITIAL_FOCUS = "initial_focus" CONF_SELECTED_DIGIT = "selected_digit" CONF_KEY_CODE = "key_code" @@ -496,6 +574,7 @@ CONF_LONG_PRESS_TIME = "long_press_time" CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" CONF_LONG_MODE = "long_mode" +CONF_MAJOR_TICKS_STYLE = "major_ticks_style" CONF_MSGBOXES = "msgboxes" CONF_OBJ = "obj" CONF_ONE_CHECKED = "one_checked" @@ -517,12 +596,15 @@ CONF_PIVOT_Y = "pivot_y" CONF_PLACEHOLDER_TEXT = "placeholder_text" CONF_POINTS = "points" CONF_PREVIOUS = "previous" +CONF_RADIUS = "radius" CONF_REPEAT_COUNT = "repeat_count" CONF_RECOLOR = "recolor" CONF_RESUME_ON_INPUT = "resume_on_input" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" +CONF_ROWS = "rows" +CONF_SCALE = "scale" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SCROLL_DIR = "scroll_dir" @@ -536,6 +618,7 @@ CONF_SRC = "src" CONF_START_ANGLE = "start_angle" CONF_START_VALUE = "start_value" CONF_STATES = "states" +CONF_STRIDE = "stride" CONF_STYLE = "style" CONF_STYLES = "styles" CONF_STYLE_DEFINITIONS = "style_definitions" @@ -544,6 +627,7 @@ CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" +CONF_TICK_STYLE = "tick_style" CONF_TIME_FORMAT = "time_format" CONF_TILE = "tile" CONF_TILE_ID = "tile_id" @@ -551,6 +635,8 @@ CONF_TILES = "tiles" CONF_TITLE = "title" CONF_TOP_LAYER = "top_layer" CONF_TOUCHSCREENS = "touchscreens" +CONF_TRANSFORM_ROTATION = "transform_rotation" +CONF_TRANSFORM_SCALE = "transform_scale" CONF_TRANSPARENCY_KEY = "transparency_key" CONF_THEME = "theme" CONF_UPDATE_ON_RELEASE = "update_on_release" @@ -578,6 +664,16 @@ LV_KEYS = LvConstant( "END", ) +LV_SCALE_MODE = LvConstant( + "LV_SCALE_MODE_", + "HORIZONTAL_TOP", + "HORIZONTAL_BOTTOM", + "VERTICAL_LEFT", + "VERTICAL_RIGHT", + "ROUND_INNER", + "ROUND_OUTER", +) + DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" @@ -590,3 +686,29 @@ def join_enums(enums, prefix=""): if prefix: return literal("|".join(f"{prefix}{e.upper()}" for e in enums)) return literal("|".join(f"(int){e.upper()}" for e in enums)) + + +# fmt: off +LV_COLOR_FORMATS = ( + "RGB565", "SWAPPED", "RGB565A8", "RGB888", "XRGB8888", "ARGB8888", "PREMULTIPLIED", "L8", "AL88", "A8", "I1", +) + +LV_DEFINES = ( + "LV_USE_FREERTOS_TASK_NOTIFY", "LV_DRAW_BUF_STRIDE_ALIGN", "LV_USE_DRAW_SW", "LV_DRAW_SW_DRAW_UNIT_CNT", + "LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D", + "LV_USE_G2D_DRAW_THREAD", "LV_VG_LITE_USE_BOX_SHADOW", "LV_VG_LITE_THORVG_16PIXELS_ALIGN", "LV_LOG_USE_TIMESTAMP", + "LV_LOG_USE_FILE_LINE", "LV_USE_OBJ_ID_BUILTIN", "LV_USE_OBJ_PROPERTY_NAME", "LV_ATTRIBUTE_MEM_ALIGN_SIZE", + "LV_FONT_MONTSERRAT_14", "LV_USE_FONT_PLACEHOLDER", "LV_WIDGETS_HAS_DEFAULT_VALUE", "LV_USE_ARCLABEL", + "LV_USE_CALENDAR", "LV_USE_CALENDAR_HEADER_ARROW", "LV_USE_CALENDAR_HEADER_DROPDOWN", "LV_USE_CHART", + "LV_USE_LIST", "LV_USE_MENU", "LV_USE_MSGBOX", "LV_USE_SCALE", + "LV_USE_TABLE", "LV_USE_SPAN", "LV_USE_WIN", "LV_USE_THEME_DEFAULT", + "LV_THEME_DEFAULT_GROW", "LV_USE_THEME_SIMPLE", "LV_USE_THEME_MONO", "LV_USE_FLEX", + "LV_USE_GRID", "LV_USE_PROFILER_BUILTIN", "LV_PROFILER_BUILTIN_DEFAULT_ENABLE", "LV_PROFILER_LAYOUT", + "LV_PROFILER_REFR", "LV_PROFILER_DRAW", "LV_PROFILER_INDEV", "LV_PROFILER_DECODER", + "LV_PROFILER_FONT", "LV_PROFILER_FS", "LV_PROFILER_TIMER", "LV_PROFILER_CACHE", + "LV_PROFILER_EVENT", "LV_USE_OBSERVER", "LV_IME_PINYIN_USE_DEFAULT_DICT", "LV_IME_PINYIN_USE_K9_MODE", + "LV_FILE_EXPLORER_QUICK_ACCESS", "LV_TEST_SCREENSHOT_CREATE_REFERENCE_IMAGE", "LV_LINUX_FBDEV_MMAP", + "LV_USE_NUTTX_MOUSE_MOVE_STEP", "LV_USE_GENERIC_MIPI", "LV_BUILD_EXAMPLES", "LV_BUILD_DEMOS", + "LV_WAYLAND_USE_EGL", "LV_WAYLAND_USE_G2D", "LV_WAYLAND_USE_SHM", "LV_LINUX_DRM_USE_EGL", + "LV_USE_LZ4", "LV_USE_THORVG", "LV_SDL_USE_EGL", "LV_USE_EGL", "LV_LABEL_LONG_TXT_HINT", "LV_LABEL_TEXT_SELECTION", +) + tuple(f"LV_DRAW_SW_SUPPORT_{f}" for f in LV_COLOR_FORMATS) diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index 259c344030..bafda8382e 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -71,7 +71,7 @@ async def encoders_to_code(var, config, default_group): lv_assign(group, lv_expr.group_create()) else: group = default_group - lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + lv.indev_set_group(listener.get_drv(), group) async def initial_focus_to_code(config): diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py index bc89470d47..f3ded6a518 100644 --- a/esphome/components/lvgl/gradient.py +++ b/esphome/components/lvgl/gradient.py @@ -7,12 +7,13 @@ from esphome.const import ( CONF_ID, CONF_POSITION, ) +from esphome.core import ID from esphome.cpp_generator import MockObj -from .defines import CONF_GRADIENTS, LV_DITHER, LV_GRAD_DIR, add_define -from .lv_validation import lv_color, lv_fraction -from .lvcode import lv_assign -from .types import lv_gradient_t +from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning +from .lv_validation import lv_color, lv_percentage, opacity +from .lvcode import lv +from .types import lv_color_t, lv_gradient_t, lv_opa_t CONF_STOPS = "stops" @@ -27,14 +28,17 @@ GRADIENT_SCHEMA = cv.ensure_list( cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t), - cv.Optional(CONF_DIRECTION, default="NONE"): LV_GRAD_DIR.one_of, + cv.Required(CONF_DIRECTION): cv.one_of( + "HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True + ), cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of, cv.Required(CONF_STOPS): cv.All( [ cv.Schema( { cv.Required(CONF_COLOR): lv_color, - cv.Required(CONF_POSITION): lv_fraction, + cv.Optional(CONF_OPA, default=1.0): opacity, + cv.Required(CONF_POSITION): lv_percentage, } ) ], @@ -47,15 +51,31 @@ GRADIENT_SCHEMA = cv.ensure_list( async def gradients_to_code(config): max_stops = 2 + if any(CONF_DITHER in x for x in config.get(CONF_GRADIENTS, ())): + add_warning( + "The 'dither' option for gradients is not supported by LVGL 9.x and will be ignored" + ) for gradient in config.get(CONF_GRADIENTS, ()): var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->") - max_stops = max(max_stops, len(gradient[CONF_STOPS])) - lv_assign(var.dir, await LV_GRAD_DIR.process(gradient[CONF_DIRECTION])) - lv_assign(var.dither, await LV_DITHER.process(gradient[CONF_DITHER])) - lv_assign(var.stops_count, len(gradient[CONF_STOPS])) - for index, stop in enumerate(gradient[CONF_STOPS]): - lv_assign(var.stops[index].color, await lv_color.process(stop[CONF_COLOR])) - lv_assign( - var.stops[index].frac, await lv_fraction.process(stop[CONF_POSITION]) - ) + idbase = gradient[CONF_ID].id + stops = gradient[CONF_STOPS] + max_stops = max(max_stops, len(stops)) + if gradient[CONF_DIRECTION].startswith("VER"): + lv.grad_vertical_init(var) + else: + lv.grad_horizontal_init(var) + stop_colors = cg.static_const_array( + ID(idbase + "_colors_", type=lv_color_t), + [await lv_color.process(x[CONF_COLOR]) for x in stops], + ) + stop_opacities = cg.static_const_array( + ID(idbase + "_opacities_", type=lv_opa_t), + [await opacity.process(x[CONF_OPA]) for x in stops], + ) + stop_positions = cg.static_const_array( + ID(idbase + "_positions_", type=cg.uint8), + [await lv_percentage.process(x[CONF_POSITION]) for x in stops], + ) + lv.grad_init_stops(var, stop_colors, stop_opacities, stop_positions, len(stops)) + add_define("LV_GRADIENT_MAX_STOPS", max_stops) diff --git a/esphome/components/lvgl/keypads.py b/esphome/components/lvgl/keypads.py index 5e2953d57f..7d8b3dd128 100644 --- a/esphome/components/lvgl/keypads.py +++ b/esphome/components/lvgl/keypads.py @@ -67,7 +67,7 @@ async def keypads_to_code(var, config, default_group): lv_assign(group, lv_expr.group_create()) else: group = default_group - lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + lv.indev_set_group(listener.get_drv(), group) async def initial_focus_to_code(config): diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index b27a0b54a2..46026852af 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -88,8 +88,8 @@ grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_spa GRID_CELL_SCHEMA = { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, - cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, - cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1), + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1), cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, } @@ -198,12 +198,8 @@ class GridLayout(Layout): { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, - cv.Optional( - CONF_GRID_CELL_ROW_SPAN, default=1 - ): cv.positive_int, - cv.Optional( - CONF_GRID_CELL_COLUMN_SPAN, default=1 - ): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1), + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1), cv.Optional( CONF_GRID_CELL_X_ALIGN, default="center" ): grid_alignments, @@ -231,8 +227,8 @@ class GridLayout(Layout): { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, - cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, - cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1), + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1), cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, }, @@ -299,11 +295,13 @@ class GridLayout(Layout): w[CONF_GRID_CELL_ROW_POS] = row w[CONF_GRID_CELL_COLUMN_POS] = column - for i in range(w[CONF_GRID_CELL_ROW_SPAN]): - for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]): + row_span = w.get(CONF_GRID_CELL_ROW_SPAN, 1) + column_span = w.get(CONF_GRID_CELL_COLUMN_SPAN, 1) + for i in range(row_span): + for j in range(column_span): if row + i >= rows or column + j >= columns: raise cv.Invalid( - f"Cell at {row}/{column} span {w[CONF_GRID_CELL_ROW_SPAN]}x{w[CONF_GRID_CELL_COLUMN_SPAN]} " + f"Cell at {row}/{column} span {row_span}x{column_span} " f"exceeds grid size {rows}x{columns}", [CONF_WIDGETS, index], ) diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h index 569f9a03c0..7309df9763 100644 --- a/esphome/components/lvgl/light/lvgl_light.h +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -38,7 +38,7 @@ class LVLight : public light::LightOutput { void set_value_(lv_color_t value) { lv_led_set_color(this->obj_, value); lv_led_on(this->obj_); - lv_event_send(this->obj_, lv_api_event, nullptr); + lv_obj_send_event(this->obj_, lv_api_event, nullptr); } lv_obj_t *obj_{}; optional initial_value_{}; diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 3c1838219c..503730098e 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -30,6 +30,7 @@ from .defines import ( LV_FONTS, LValidator, LvConstant, + StaticCastExpression, call_lambda, literal, ) @@ -40,22 +41,28 @@ from .helpers import ( lv_fonts_used, requires_component, ) -from .types import lv_gradient_t +from .types import lv_gradient_t, lv_opa_t -opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") +LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER") @schema_extractor("one_of") def opacity_validator(value): if value == SCHEMA_EXTRACT: - return opacity_consts.choices - value = cv.Any(cv.percentage, opacity_consts.one_of)(value) - if isinstance(value, float): - return int(value * 255) - return value + return LV_OPA.choices + value = cv.Any(cv.percentage, LV_OPA.one_of)(value) + if value == str(LV_OPA.COVER): + value = 1.0 + if value == str(LV_OPA.TRANSP): + value = 0.0 + return cv.float_range(0.0, 1.0)(value) -opacity = LValidator(opacity_validator, uint32, retmapper=literal) +opacity = LValidator( + opacity_validator, + lv_opa_t, + retmapper=lambda opa: StaticCastExpression(cg.uint8, opa * 255.0), +) COLOR_NAMES = { "aliceblue": 0xF0F8FF, @@ -244,7 +251,17 @@ def option_string(value): return value -lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) +class LvColor(LValidator): + def __init__(self): + super().__init__(color, ty.lv_color_t, retmapper=color_retmapper) + + def __getattr__(self, item): + if item in COLOR_NAMES: + return color_retmapper(COLOR_NAMES[item]) + raise AttributeError(item) + + +lv_color = LvColor() def pixels_or_percent_validator(value): @@ -252,16 +269,17 @@ def pixels_or_percent_validator(value): if value == SCHEMA_EXTRACT: return ["pixels", "..%"] if isinstance(value, str) and value.lower().endswith("px"): - value = cv.int_(value[:-2]) + return cv.int_(value[:-2]) if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value): - return value - value = cv.Any(cv.int_, cv.percentage)(value) - if isinstance(value, int): - return value - return f"lv_pct({int(value * 100)})" + return int(value[6:-1]) / 100.0 + return cv.Any(cv.int_, cv.possibly_negative_percentage)(value) -pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal) +pixels_or_percent = LValidator( + pixels_or_percent_validator, + uint32, + retmapper=lambda x: x if isinstance(x, int) else literal(f"lv_pct({int(x * 100)})"), +) def pixels_validator(value): @@ -282,15 +300,11 @@ def padding_validator(value): padding = LValidator(padding_validator, int32, retmapper=literal) -def zoom_validator(value): +def scale_validator(value): return cv.float_range(0.1, 10.0)(value) -def zoom_retmapper(value): - return int(value * 256) - - -zoom = LValidator(zoom_validator, uint32, retmapper=zoom_retmapper) +scale = LValidator(scale_validator, uint32, retmapper=lambda x: int(x * 256)) def angle(value): @@ -321,17 +335,23 @@ def size_validator(value): return pixels_or_percent_validator(value) -size = LValidator(size_validator, uint32, retmapper=literal) +size = LValidator( + size_validator, + uint32, + retmapper=lambda x: ( + literal(x) if isinstance(x, str) else pixels_or_percent.retmapper(x) + ), +) -radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") +LV_RADIUS = LvConstant("LV_RADIUS_", "CIRCLE") @schema_extractor("one_of") def fraction_validator(value): if value == SCHEMA_EXTRACT: - return radius_consts.choices - value = cv.Any(size, cv.percentage, radius_consts.one_of)(value) + return LV_RADIUS.choices + value = cv.Any(size, cv.percentage, LV_RADIUS.one_of)(value) if isinstance(value, float): return int(value * 255) return value @@ -374,12 +394,6 @@ lv_image_list = LValidator( lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) -def lv_pct(value: int | float): - if isinstance(value, float): - value = int(value * 100) - return literal(f"lv_pct({value})") - - def lvms_validator_(value): if value == "never": value = "2147483647ms" @@ -424,30 +438,28 @@ class TextValidator(LValidator): if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): - time_format = cpp_string_escape(time_format) - return cg.RawExpression( + source = MockObj( call_lambda( await cg.process_lambda(source, args, return_type=ESPTime) ) - + f".strftime({time_format}).c_str()" ) # must be an ID - source = await cg.get_variable(source) - return source.now().strftime(time_format).c_str() + else: + source = (await cg.get_variable(source)).now() + return source.strftime(time_format).c_str() if isinstance(value, Lambda): value = call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) ) + textvalue = str(value) # Was the lambda call reduced to a string? - if value.endswith("c_str()") or ( - value.endswith('"') and value.startswith('"') + if textvalue.endswith("c_str()") or ( + textvalue.endswith('"') and textvalue.startswith('"') ): - pass - else: - # Either a std::string or a lambda call returning that. We need const char* - value = f"({value}).c_str()" - return cg.RawExpression(value) + return value + # Either a std::string or a lambda call returning that. We need const char* + return MockObj(f"({value}).c_str()") return await super().process(value, args) @@ -455,21 +467,24 @@ lv_text = TextValidator() lv_float = LValidator(cv.float_, cg.float_) lv_int = LValidator(cv.int_, cg.int_) lv_positive_int = LValidator(cv.positive_int, cg.int_) -lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) -def gradient_mapper(value): - return MockObj(value) +def _percentage_validator(value): + value = cv.Any(cv.percentage, cv.float_range(0.0, 1.0), cv.int_range(0, 255))(value) + if isinstance(value, int): + return value / 255.0 + return value -def gradient_validator(value): - return cv.use_id(lv_gradient_t)(value) +lv_percentage = LValidator( + _percentage_validator, cg.float_, retmapper=lambda x: int(x * 255) +) lv_gradient = LValidator( - validator=gradient_validator, + validator=cv.use_id(lv_gradient_t), rtype=lv_gradient_t, - retmapper=gradient_mapper, + retmapper=MockObj, ) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index b79d1e88dd..146b261f26 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -168,6 +168,9 @@ class LambdaContext(CodeContext): def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: return self.parameters + def get_parameter(self, index: int): + return literal(self.parameters[index][1]) + async def __aenter__(self): await super().__aenter__() add_line_marks(self.where) @@ -250,10 +253,14 @@ class MockLv: A mock object that can be used to generate LVGL calls. """ + # Mapping for LVGL 9 + ATTR_MAP = {"event_send": "obj_send_event", "dither": "bg_dither_mode"} + def __init__(self, base): self.base = base def __getattr__(self, attr: str) -> "MockLv": + attr = MockLv.ATTR_MAP.get(attr, attr) return MockLv(f"{self.base}{attr}") def append(self, expression): @@ -307,6 +314,7 @@ class ReturnStatement(ExpressionStatement): class LvExpr(MockLv): def __getattr__(self, attr: str) -> "MockLv": + attr = MockLv.ATTR_MAP.get(attr, attr) return LvExpr(f"{self.base}{attr}") def append(self, expression): diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 5400054bb1..b3cb4d56ad 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -2,16 +2,21 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "lvgl_hal.h" #include "lvgl_esphome.h" +#include "core/lv_global.h" +#include "core/lv_obj_class_private.h" + #include -namespace esphome { -namespace lvgl { +static void *lv_alloc_draw_buf(size_t size, bool internal); +static void *draw_buf_alloc_cb(size_t size, lv_color_format_t color_format) { return lv_alloc_draw_buf(size, false); }; + +namespace esphome::lvgl { static const char *const TAG = "lvgl"; -static const size_t MIN_BUFFER_FRAC = 8; +static const size_t MIN_BUFFER_FRAC = 8; // buffer must be at least 1/8 of the display size +static const size_t MIN_BUFFER_SIZE = 2048; // Sensible minimum buffer size static const char *const EVENT_NAMES[] = { "NONE", @@ -61,7 +66,14 @@ static const char *const EVENT_NAMES[] = { "GET_SELF_SIZE", }; -std::string lv_event_code_name_for(uint8_t event_code) { +static const unsigned LOG_LEVEL_MAP[] = { + ESPHOME_LOG_LEVEL_DEBUG, ESPHOME_LOG_LEVEL_INFO, ESPHOME_LOG_LEVEL_WARN, + ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_NONE, + +}; + +std::string lv_event_code_name_for(lv_event_t *event) { + auto event_code = lv_event_get_code(event); if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) { return EVENT_NAMES[event_code]; } @@ -71,11 +83,12 @@ std::string lv_event_code_name_for(uint8_t event_code) { return buf; } -static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { +static void rounder_cb(lv_event_t *event) { + auto *comp = static_cast(lv_event_get_user_data(event)); + auto *area = static_cast(lv_event_get_param(event)); // cater for display driver chips with special requirements for bounds of partial // draw areas. Extend the draw area to satisfy: // * Coordinates must be a multiple of draw_rounding - auto *comp = static_cast(disp_drv->user_data); auto draw_rounding = comp->draw_rounding; // round down the start coordinates area->x1 = area->x1 / draw_rounding * draw_rounding; @@ -85,15 +98,14 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1; } -void LvglComponent::monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px) { - ESP_LOGVV(TAG, "Draw end: %" PRIu32 " pixels in %" PRIu32 " ms", px, time); - auto *comp = static_cast(disp_drv->user_data); +void LvglComponent::render_end_cb(lv_event_t *event) { + auto *comp = static_cast(lv_event_get_user_data(event)); comp->draw_end_(); } -void LvglComponent::render_start_cb(lv_disp_drv_t *disp_drv) { +void LvglComponent::render_start_cb(lv_event_t *event) { ESP_LOGVV(TAG, "Draw start"); - auto *comp = static_cast(disp_drv->user_data); + auto *comp = static_cast(lv_event_get_user_data(event)); comp->draw_start_(); } @@ -106,16 +118,15 @@ void LvglComponent::dump_config() { " Buffer size: %zu%%\n" " Rotation: %d\n" " Draw rounding: %d", - this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation, - (int) this->draw_rounding); + this->width_, this->height_, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding); } void LvglComponent::set_paused(bool paused, bool show_snow) { this->paused_ = paused; this->show_snow_ = show_snow; - if (!paused && lv_scr_act() != nullptr) { - lv_disp_trig_activity(this->disp_); // resets the inactivity time - lv_obj_invalidate(lv_scr_act()); + if (!paused && lv_screen_active() != nullptr) { + lv_display_trigger_activity(this->disp_); // resets the inactivity time + lv_obj_invalidate(lv_screen_active()); } if (paused && this->pause_callback_ != nullptr) this->pause_callback_->trigger(); @@ -125,6 +136,14 @@ void LvglComponent::set_paused(bool paused, bool show_snow) { void LvglComponent::esphome_lvgl_init() { lv_init(); + // override draw buf alloc to ensure proper alignment for PPA + LV_GLOBAL_DEFAULT()->draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb; + LV_GLOBAL_DEFAULT()->draw_buf_handlers.buf_free_cb = lv_free_core; + LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb; + LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers.buf_free_cb = lv_free_core; + LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb; + LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_free_cb = lv_free_core; + lv_tick_set_cb([] { return millis(); }); lv_update_event = static_cast(lv_event_register_id()); lv_api_event = static_cast(lv_event_register_id()); } @@ -149,7 +168,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); page->set_parent(this); - lv_disp_set_default(this->disp_); + lv_display_set_default(this->disp_); page->setup(this->pages_.size() - 1); } @@ -157,7 +176,11 @@ void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t ti if (index >= this->pages_.size()) return; this->current_page_ = index; - lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + if (anim == LV_SCREEN_LOAD_ANIM_NONE) { + lv_scr_load(this->pages_[this->current_page_]->obj); + } else { + lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); + } } void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { @@ -187,13 +210,13 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { size_t LvglComponent::get_current_page() const { return this->current_page_; } bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } -void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { +void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; - lv_color_t *dst = this->rotate_buf_; + lv_color_data *dst = reinterpret_cast(this->rotate_buf_); switch (this->rotation) { case display::DISPLAY_ROTATION_90_DEGREES: for (lv_coord_t x = height; x-- != 0;) { @@ -202,7 +225,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { } } y1 = x1; - x1 = this->disp_drv_.ver_res - area->y1 - height; + x1 = this->height_ - area->y1 - height; height = width; width = height_rounded; break; @@ -213,8 +236,8 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { dst[y * width + x] = *ptr++; } } - x1 = this->disp_drv_.hor_res - x1 - width; - y1 = this->disp_drv_.ver_res - y1 - height; + x1 = this->width_ - x1 - width; + y1 = this->height_ - y1 - height; break; case display::DISPLAY_ROTATION_270_DEGREES: @@ -224,7 +247,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { } } x1 = y1; - y1 = this->disp_drv_.hor_res - area->x1 - width; + y1 = this->width_ - area->x1 - width; height = width; width = height_rounded; break; @@ -234,20 +257,19 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { break; } for (auto *display : this->displays_) { - ESP_LOGV(TAG, "draw buffer x1=%d, y1=%d, width=%d, height=%d", x1, y1, width, height); display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) dst, display::COLOR_ORDER_RGB, LV_BITNESS, - LV_COLOR_16_SWAP); + this->big_endian_); } } -void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { +void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) { if (!this->is_paused()) { auto now = millis(); - this->draw_buffer_(area, color_p); - ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + this->draw_buffer_(area, reinterpret_cast(color_p)); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1, + (int) lv_area_get_width(area), (int) lv_area_get_height(area), (int) (millis() - now)); } - lv_disp_flush_ready(disp_drv); + lv_display_flush_ready(disp_drv); } IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { @@ -264,14 +286,14 @@ IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeo #ifdef USE_LVGL_TOUCHSCREEN LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) { this->set_parent(parent); - lv_indev_drv_init(&this->drv_); - this->drv_.disp = parent->get_disp(); - this->drv_.long_press_repeat_time = long_press_repeat_time; - this->drv_.long_press_time = long_press_time; - this->drv_.type = LV_INDEV_TYPE_POINTER; - this->drv_.user_data = this; - this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { - auto *l = static_cast(d->user_data); + this->drv_ = lv_indev_create(); + lv_indev_set_type(this->drv_, LV_INDEV_TYPE_POINTER); + lv_indev_set_disp(this->drv_, parent->get_disp()); + lv_indev_set_long_press_time(this->drv_, long_press_time); + // long press repeat time TBD + lv_indev_set_user_data(this->drv_, this); + lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) { + auto *l = static_cast(lv_indev_get_user_data(d)); if (l->touch_pressed_) { data->point.x = l->touch_point_.x; data->point.y = l->touch_point_.y; @@ -279,7 +301,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r } else { data->state = LV_INDEV_STATE_RELEASED; } - }; + }); } void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { @@ -289,21 +311,78 @@ void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { } #endif // USE_LVGL_TOUCHSCREEN +#ifdef USE_LVGL_METER + +int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value) { + auto *scale = lv_obj_get_parent(obj); + auto min_value = lv_scale_get_range_min_value(scale); + return ((value - min_value) * lv_scale_get_angle_range(scale) / (lv_scale_get_range_max_value(scale) - min_value) + + lv_scale_get_rotation((scale))) % + 360; +} + +void IndicatorLine::set_obj(lv_obj_t *lv_obj) { + LvCompound::set_obj(lv_obj); + lv_line_set_points(lv_obj, this->points_, 2); + lv_obj_add_event_cb( + lv_obj_get_parent(obj), + [](lv_event_t *e) { + auto *indicator = static_cast(lv_event_get_user_data(e)); + indicator->update_length_(); + ESP_LOGV(TAG, "Updated length, value = %d", indicator->angle_); + }, + LV_EVENT_SIZE_CHANGED, this); +} + +void IndicatorLine::set_value(int value) { + auto angle = lv_get_needle_angle_for_value(this->obj, value); + if (angle != this->angle_) { + this->angle_ = angle; + this->update_length_(); + } +} + +void IndicatorLine::update_length_() { + uint32_t actual_needle_length; + auto radius = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2; + auto length = lv_obj_get_style_length(this->obj, LV_PART_MAIN); + auto radial_offset = lv_obj_get_style_radial_offset(this->obj, LV_PART_MAIN); + if (LV_COORD_IS_PCT(radial_offset)) { + radial_offset = radius * LV_COORD_GET_PCT(radial_offset) / 100; + } + if (LV_COORD_IS_PCT(length)) { + actual_needle_length = radius * LV_COORD_GET_PCT(length) / 100; + } else if (length < 0) { + actual_needle_length = radius + length; + } else { + actual_needle_length = length; + } + auto x = lv_trigo_cos(this->angle_) / 32768.0f; + auto y = lv_trigo_sin(this->angle_) / 32768.0f; + this->points_[0].x = radius + radial_offset * x; + this->points_[0].y = radius + radial_offset * y; + this->points_[1].x = x * actual_needle_length + radius; + this->points_[1].y = y * actual_needle_length + radius; + lv_obj_refresh_self_size(this->obj); + lv_obj_invalidate(this->obj); +} +#endif + #ifdef USE_LVGL_KEY_LISTENER -LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { - lv_indev_drv_init(&this->drv_); - this->drv_.type = type; - this->drv_.user_data = this; - this->drv_.long_press_time = lpt; - this->drv_.long_press_repeat_time = lprt; - this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { - auto *l = static_cast(d->user_data); +LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time) { + this->drv_ = lv_indev_create(); + lv_indev_set_type(this->drv_, type); + lv_indev_set_long_press_time(this->drv_, long_press_time); + lv_indev_set_long_press_repeat_time(this->drv_, long_press_repeat_time); + lv_indev_set_user_data(this->drv_, this); + lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) { + auto *l = static_cast(lv_indev_get_user_data(d)); data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; data->key = l->key_; data->enc_diff = (int16_t) (l->count_ - l->last_count_); l->last_count_ = l->count_; data->continue_reading = false; - }; + }); } #endif // USE_LVGL_KEY_LISTENER @@ -325,7 +404,7 @@ void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t a auto index = std::find(this->options_.begin(), this->options_.end(), text); if (index != this->options_.end()) { this->set_selected_index(index - this->options_.begin(), anim); - lv_event_send(this->obj, lv_api_event, nullptr); + lv_obj_send_event(this->obj, lv_api_event, nullptr); } } @@ -335,7 +414,7 @@ void LvSelectable::set_options(std::vector options) { index = options.size() - 1; this->options_ = std::move(options); this->set_option_string(join_string(this->options_).c_str()); - lv_event_send(this->obj, LV_EVENT_REFRESH, nullptr); + lv_obj_send_event(this->obj, LV_EVENT_REFRESH, nullptr); this->set_selected_index(index, LV_ANIM_OFF); } #endif // USE_LVGL_DROPDOWN || LV_USE_ROLLER @@ -346,17 +425,17 @@ void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) { lv_obj_add_event_cb( lv_obj, [](lv_event_t *event) { - auto *self = static_cast(event->user_data); + auto *self = static_cast(lv_event_get_user_data(event)); if (self->key_callback_.size() == 0) return; - auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); - if (key_idx == LV_BTNMATRIX_BTN_NONE) + auto key_idx = lv_buttonmatrix_get_selected_button(self->obj); + if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE) return; if (self->key_map_.count(key_idx) != 0) { self->send_key_(self->key_map_[key_idx]); return; } - const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx); + const auto *str = lv_buttonmatrix_get_button_text(self->obj, key_idx); auto len = strlen(str); while (len--) self->send_key_(*str++); @@ -376,14 +455,14 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { lv_obj_add_event_cb( lv_obj, [](lv_event_t *event) { - auto *self = static_cast(event->user_data); + auto *self = static_cast(lv_event_get_user_data(event)); if (self->key_callback_.size() == 0) return; - auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); - if (key_idx == LV_BTNMATRIX_BTN_NONE) + auto key_idx = lv_buttonmatrix_get_selected_button(self->obj); + if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE) return; - const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx); + const char *txt = lv_buttonmatrix_get_button_text(self->obj, key_idx); if (txt == nullptr) return; for (const auto *kb_special_key : KB_SPECIAL_KEYS) { @@ -419,33 +498,29 @@ bool LvglComponent::is_paused() const { } void LvglComponent::write_random_() { - int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000; + int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000; if (iterations <= 0) iterations = 1; while (iterations-- != 0) { - auto col = random_uint32() % this->disp_drv_.hor_res; + int32_t col = random_uint32() % this->width_; col = col / this->draw_rounding * this->draw_rounding; - auto row = random_uint32() % this->disp_drv_.ver_res; + int32_t row = random_uint32() % this->height_; row = row / this->draw_rounding * this->draw_rounding; - auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1; - // clamp size so the square fits within the draw buffer - if ((size + 1) * (size + 1) > this->draw_buf_.size) - size = static_cast(sqrtf(this->draw_buf_.size)) - 1; - lv_area_t area; - area.x1 = col; - area.y1 = row; - area.x2 = col + size; - area.y2 = row + size; - if (area.x2 >= this->disp_drv_.hor_res) - area.x2 = this->disp_drv_.hor_res - 1; - if (area.y2 >= this->disp_drv_.ver_res) - area.y2 = this->disp_drv_.ver_res - 1; + // size will be between 8 and 32, and a multiple of draw_rounding + int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding; + lv_area_t area{col, row, col + size - 1, row + size - 1}; + // clip to display bounds just in case + if (area.x2 >= this->width_) + area.x2 = this->width_ - 1; + if (area.y2 >= this->height_) + area.y2 = this->height_ - 1; + // line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2; for (size_t i = 0; i != line_len; i++) { - ((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32(); + ((uint32_t *) (this->draw_buf_))[i] = random_uint32(); } - this->draw_buffer_(&area, (lv_color_t *) this->draw_buf_.buf1); + this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_); } } @@ -477,38 +552,32 @@ LvglComponent::LvglComponent(std::vector displays, float buf full_refresh_(full_refresh), resume_on_input_(resume_on_input), update_when_display_idle_(update_when_display_idle) { - lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0); - lv_disp_drv_init(&this->disp_drv_); - this->disp_drv_.draw_buf = &this->draw_buf_; - this->disp_drv_.user_data = this; - this->disp_drv_.full_refresh = this->full_refresh_; - this->disp_drv_.flush_cb = static_flush_cb; - this->disp_drv_.rounder_cb = rounder_cb; - this->disp_ = lv_disp_drv_register(&this->disp_drv_); + this->disp_ = lv_display_create(240, 240); } void LvglComponent::setup() { auto *display = this->displays_[0]; auto rounding = this->draw_rounding; // cater for displays with dimensions that don't divide by the required rounding + this->width_ = display->get_width(); + this->height_ = display->get_height(); auto width = (display->get_width() + rounding - 1) / rounding * rounding; auto height = (display->get_height() + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; - size_t buffer_pixels = width * height / frac; - auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; + auto buf_bytes = clamp_at_least(width * height / frac * LV_COLOR_DEPTH / 8, MIN_BUFFER_SIZE); void *buffer = nullptr; + // for small buffers, try to allocate in internal memory first to improve performance if (this->buffer_frac_ >= MIN_BUFFER_FRAC / 2) - buffer = malloc(buf_bytes); // NOLINT + buffer = lv_alloc_draw_buf(buf_bytes, true); // NOLINT if (buffer == nullptr) - buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT + buffer = lv_alloc_draw_buf(buf_bytes, false); // NOLINT // if specific buffer size not set and can't get 100%, try for a smaller one if (buffer == nullptr && this->buffer_frac_ == 0) { frac = MIN_BUFFER_FRAC; - buffer_pixels /= MIN_BUFFER_FRAC; buf_bytes /= MIN_BUFFER_FRAC; - buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT + buffer = lv_alloc_draw_buf(buf_bytes, false); // NOLINT } this->buffer_frac_ = frac; if (buffer == nullptr) { @@ -516,13 +585,17 @@ void LvglComponent::setup() { this->mark_failed(); return; } - lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); - this->disp_drv_.hor_res = display->get_width(); - this->disp_drv_.ver_res = display->get_height(); - lv_disp_drv_update(this->disp_, &this->disp_drv_); + this->draw_buf_ = static_cast(buffer); + lv_display_set_resolution(this->disp_, this->width_, this->height_); + lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565); + lv_display_set_flush_cb(this->disp_, static_flush_cb); + lv_display_set_user_data(this->disp_, this); + lv_display_add_event_cb(this->disp_, rounder_cb, LV_EVENT_INVALIDATE_AREA, this); + lv_display_set_buffers(this->disp_, this->draw_buf_, nullptr, buf_bytes, + this->full_refresh_ ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL); this->rotation = display->get_rotation(); if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { - this->rotate_buf_ = static_cast(lv_custom_mem_alloc(buf_bytes)); // NOLINT + this->rotate_buf_ = static_cast(lv_alloc_draw_buf(buf_bytes, false)); // NOLINT if (this->rotate_buf_ == nullptr) { this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); @@ -530,26 +603,28 @@ void LvglComponent::setup() { } } if (this->draw_start_callback_ != nullptr) { - this->disp_drv_.render_start_cb = render_start_cb; + lv_display_add_event_cb(this->disp_, render_start_cb, LV_EVENT_RENDER_START, this); } if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) { - this->disp_drv_.monitor_cb = monitor_cb; + lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this); } #if LV_USE_LOG - lv_log_register_print_cb([](const char *buf) { + lv_log_register_print_cb([](lv_log_level_t level, const char *buf) { auto next = strchr(buf, ')'); if (next != nullptr) buf = next + 1; while (isspace(*buf)) buf++; - esp_log_printf_(LVGL_LOG_LEVEL, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); + if (level >= sizeof(LOG_LEVEL_MAP) / sizeof(LOG_LEVEL_MAP[0])) + level = sizeof(LOG_LEVEL_MAP) / sizeof(LOG_LEVEL_MAP[0]) - 1; + esp_log_printf_(LOG_LEVEL_MAP[level], TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); }); #endif // Rotation will be handled by our drawing function, so reset the display rotation. for (auto *disp : this->displays_) disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); - this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); - lv_disp_trig_activity(this->disp_); + this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0); + lv_display_trigger_activity(this->disp_); } void LvglComponent::update() { @@ -557,7 +632,7 @@ void LvglComponent::update() { if (this->is_paused()) { return; } - this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); + this->idle_callbacks_.call(lv_display_get_inactive_time(this->disp_)); } void LvglComponent::loop() { @@ -565,41 +640,131 @@ void LvglComponent::loop() { if (this->paused_ && this->show_snow_) this->write_random_(); } else { - lv_timer_handler_run_in_period(5); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + auto now = millis(); + lv_timer_handler(); + auto elapsed = millis() - now; + if (elapsed > 15) { + ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now)); + } +#else + lv_timer_handler(); +#endif } } #ifdef USE_LVGL_ANIMIMG void lv_animimg_stop(lv_obj_t *obj) { - auto *animg = (lv_animimg_t *) obj; - int32_t duration = animg->anim.time; + int32_t duration = lv_animimg_get_duration(obj); lv_animimg_set_duration(obj, 0); lv_animimg_start(obj); lv_animimg_set_duration(obj, duration); } #endif -void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - reinterpret_cast(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); +void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) { + reinterpret_cast(lv_display_get_user_data(disp_drv))->flush_cb_(disp_drv, area, color_p); } -} // namespace lvgl -} // namespace esphome -size_t lv_millis(void) { return esphome::millis(); } +#ifdef USE_LVGL_SCALE +/** + * Function to apply colors to ticks based on position + * @param e The event data + * @param color_start The color to apply to the first tick + * @param color_end The color to apply to the last tick + * @param width + */ +void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, + lv_color_t color_end, int width, bool local) { + auto *scale = static_cast(lv_event_get_target(e)); + lv_draw_task_t *task = lv_event_get_draw_task(e); + + if (lv_draw_task_get_type(task) == LV_DRAW_TASK_TYPE_LINE) { + auto *line_dsc = static_cast(lv_draw_task_get_draw_dsc(task)); + auto tick = line_dsc->base.id1; + if (tick >= range_start && tick <= range_end) { + unsigned range = range_end - range_start; + if (local) { + tick -= range_start; + } else { + range = lv_scale_get_total_tick_count(scale) - 1; + } + if (range == 0) + range = 1; + auto ratio = (tick * 255) / range; + line_dsc->color = lv_color_mix(color_end, color_start, ratio); + line_dsc->width += width; + } + } +} +#endif // USE_LVGL_SCALE + +static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) { + LV_TRACE_OBJ_CREATE("begin"); + LV_UNUSED(class_p); +} + +// Container class. Name is based on LVGL naming convention but upper case to keep ESPHome clang-tidy happy +const lv_obj_class_t LV_CONTAINER_CLASS = { + .base_class = &lv_obj_class, + .constructor_cb = lv_container_constructor, + .name = "lv_container", +}; + +lv_obj_t *lv_container_create(lv_obj_t *parent) { + lv_obj_t *obj = lv_obj_class_create_obj(&LV_CONTAINER_CLASS, parent); + lv_obj_class_init_obj(obj); + return obj; +} +} // namespace esphome::lvgl + +lv_result_t lv_mem_test_core() { return LV_RESULT_OK; } + +void lv_mem_init() {} + +void lv_mem_deinit() {} #if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266) -void *lv_custom_mem_alloc(size_t size) { +void *lv_malloc_core(size_t size) { auto *ptr = malloc(size); // NOLINT if (ptr == nullptr) { ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); } return ptr; } -void lv_custom_mem_free(void *ptr) { return free(ptr); } // NOLINT -void *lv_custom_mem_realloc(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT -#else +void lv_free_core(void *ptr) { return free(ptr); } // NOLINT +void *lv_realloc_core(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT + +void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) { memset(mon_p, 0, sizeof(lv_mem_monitor_t)); } +static void *lv_alloc_draw_buf(size_t size, bool internal) { + return malloc(size); // NOLINT +} + +#elif defined(USE_ESP32) static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT -void *lv_custom_mem_alloc(size_t size) { +static void *lv_alloc_draw_buf(size_t size, bool internal) { + void *buffer; + size = ((size + LV_DRAW_BUF_ALIGN - 1) / LV_DRAW_BUF_ALIGN) * LV_DRAW_BUF_ALIGN; + buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT + if (buffer == nullptr) + ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : ""); + return buffer; +} + +void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) { + multi_heap_info_t heap_info; + heap_caps_get_info(&heap_info, cap_bits); + mon_p->total_size = heap_info.total_allocated_bytes + heap_info.total_free_bytes; + mon_p->free_size = heap_info.total_free_bytes; + mon_p->max_used = heap_info.total_allocated_bytes; + mon_p->free_biggest_size = heap_info.largest_free_block; + mon_p->used_cnt = heap_info.allocated_blocks; + mon_p->free_cnt = heap_info.free_blocks; + mon_p->used_pct = heap_info.allocated_blocks * 100 / (heap_info.allocated_blocks + heap_info.free_blocks); + mon_p->frag_pct = 0; +} + +void *lv_malloc_core(size_t size) { void *ptr; ptr = heap_caps_malloc(size, cap_bits); if (ptr == nullptr) { @@ -614,14 +779,14 @@ void *lv_custom_mem_alloc(size_t size) { return ptr; } -void lv_custom_mem_free(void *ptr) { +void lv_free_core(void *ptr) { ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); if (ptr == nullptr) return; heap_caps_free(ptr); } -void *lv_custom_mem_realloc(void *ptr, size_t size) { +void *lv_realloc_core(void *ptr, size_t size) { ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); return heap_caps_realloc(ptr, size, cap_bits); } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index aa8dd2fba5..8e34f16c98 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -4,7 +4,7 @@ #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif // USE_BINARY_SENSOR -#ifdef USE_LVGL_IMAGE +#ifdef USE_IMAGE #include "esphome/components/image/image.h" #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ROTARY_ENCODER @@ -19,16 +19,17 @@ #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" #include "esphome/core/component.h" -#include "esphome/core/log.h" + +#include #include #include #include #include -#ifdef USE_LVGL_FONT +#ifdef USE_FONT #include "esphome/components/font/font.h" #endif // USE_LVGL_FONT -#ifdef USE_LVGL_TOUCHSCREEN +#ifdef USE_TOUCHSCREEN #include "esphome/components/touchscreen/touchscreen.h" #endif // USE_LVGL_TOUCHSCREEN @@ -36,12 +37,24 @@ #include "esphome/components/key_provider/key_provider.h" #endif // USE_LVGL_BUTTONMATRIX -namespace esphome { -namespace lvgl { +namespace esphome::lvgl { + +#if LV_COLOR_DEPTH == 16 +using lv_color_data = uint16_t; +#endif +#if LV_COLOR_DEPTH == 32 +using lv_color_data = uint32_t; +#endif extern lv_event_code_t lv_api_event; // NOLINT extern lv_event_code_t lv_update_event; // NOLINT -extern std::string lv_event_code_name_for(uint8_t event_code); +extern std::string lv_event_code_name_for(lv_event_t *event); + +lv_obj_t *lv_container_create(lv_obj_t *parent); +#ifdef USE_LVGL_SCALE +void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start, + lv_color_t color_end, int width, bool local); +#endif #if LV_COLOR_DEPTH == 16 static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; #elif LV_COLOR_DEPTH == 32 @@ -50,7 +63,7 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; #endif // LV_COLOR_DEPTH -#ifdef USE_LVGL_FONT +#if defined(USE_FONT) && defined(USE_LVGL_FONT) inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) { lv_obj_set_style_text_font(obj, font->get_lv_font(), part); } @@ -58,50 +71,37 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { lv_style_set_text_font(style, font->get_lv_font()); } #endif -#ifdef USE_LVGL_IMAGE +#ifdef USE_IMAGE // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. -inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) { - lv_img_set_src(obj, image->get_lv_img_dsc()); -} -inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) { - lv_disp_set_bg_image(disp, image->get_lv_img_dsc()); +inline void lv_image_set_src(lv_obj_t *obj, esphome::image::Image *image) { + lv_image_set_src(obj, image->get_lv_image_dsc()); } -inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) { - lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector); +inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) { + lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); } -#ifdef USE_LVGL_CANVAS -inline void lv_canvas_draw_img(lv_obj_t *canvas, lv_coord_t x, lv_coord_t y, image::Image *image, - lv_draw_img_dsc_t *dsc) { - lv_canvas_draw_img(canvas, x, y, image->get_lv_img_dsc(), dsc); -} -#endif - -#ifdef USE_LVGL_METER -inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src, - lv_coord_t pivot_x, lv_coord_t pivot_y) { - return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y); -} -#endif // USE_LVGL_METER #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector images) { - auto *dsc = static_cast *>(lv_obj_get_user_data(img)); + auto *dsc = static_cast *>(lv_obj_get_user_data(img)); if (dsc == nullptr) { // object will be lazily allocated but never freed. - dsc = new std::vector(images.size()); // NOLINT + dsc = new std::vector(images.size()); // NOLINT lv_obj_set_user_data(img, dsc); } dsc->clear(); for (auto &image : images) { - dsc->push_back(image->get_lv_img_dsc()); + dsc->push_back(image->get_lv_image_dsc()); } lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size()); } - #endif // USE_LVGL_ANIMIMG +#ifdef USE_LVGL_METER +int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value); +#endif + // Parent class for things that wrap an LVGL object class LvCompound { public: @@ -130,7 +130,7 @@ class LvPageType : public Parented { using LvLambdaType = std::function; using set_value_lambda_t = std::function; -using event_callback_t = void(_lv_event_t *); +using event_callback_t = void(lv_event_t *); using text_lambda_t = std::function; template class ObjUpdateAction : public Action { @@ -152,7 +152,7 @@ class LvglComponent : public PollingComponent { public: LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, int draw_rounding, bool resume_on_input, bool update_when_display_idle); - static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); + static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } void setup() override; @@ -160,11 +160,11 @@ class LvglComponent : public PollingComponent { void loop() override; template void add_on_idle_callback(F &&callback) { this->idle_callbacks_.add(std::forward(callback)); } - static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px); - static void render_start_cb(lv_disp_drv_t *disp_drv); + static void render_end_cb(lv_event_t *event); + static void render_start_cb(lv_event_t *event); void dump_config() override; lv_disp_t *get_disp() { return this->disp_; } - lv_obj_t *get_scr_act() { return lv_disp_get_scr_act(this->disp_); } + lv_obj_t *get_screen_active() { return lv_display_get_screen_active(this->disp_); } // Pause or resume the display. // @param paused If true, pause the display. If false, resume the display. // @param show_snow If true, show the snow effect when paused. @@ -187,11 +187,13 @@ class LvglComponent : public PollingComponent { static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, lv_event_code_t event3); + void add_page(LvPageType *page); void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); void show_next_page(lv_scr_load_anim_t anim, uint32_t time); void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } + void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; } size_t get_current_page() const; void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); } void restore_focus_mark(lv_group_t *group) { @@ -216,22 +218,25 @@ class LvglComponent : public PollingComponent { void draw_start_() const { this->draw_start_callback_->trigger(); } void write_random_(); - void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); - void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); + void draw_buffer_(const lv_area_t *area, lv_color_data *ptr); + void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); + std::vector displays_{}; size_t buffer_frac_{1}; bool full_refresh_{}; bool resume_on_input_{}; bool update_when_display_idle_{}; - lv_disp_draw_buf_t draw_buf_{}; - lv_disp_drv_t disp_drv_{}; - lv_disp_t *disp_{}; + uint8_t *draw_buf_{}; + lv_display_t *disp_{}; + uint16_t width_{}; + uint16_t height_{}; bool paused_{}; std::vector pages_{}; size_t current_page_{0}; bool show_snow_{}; bool page_wrap_{true}; + bool big_endian_{}; std::map focus_marks_{}; CallbackManager idle_callbacks_{}; @@ -239,7 +244,7 @@ class LvglComponent : public PollingComponent { Trigger<> *resume_callback_{}; Trigger<> *draw_start_callback_{}; Trigger<> *draw_end_callback_{}; - lv_color_t *rotate_buf_{}; + void *rotate_buf_{}; }; class IdleTrigger : public Trigger<> { @@ -278,19 +283,37 @@ class LVTouchListener : public touchscreen::TouchListener, public Parentedparent_->maybe_wakeup(); } - lv_indev_drv_t *get_drv() { return &this->drv_; } + lv_indev_t *get_drv() { return this->drv_; } protected: - lv_indev_drv_t drv_{}; + lv_indev_t *drv_{}; touchscreen::TouchPoint touch_point_{}; bool touch_pressed_{}; }; #endif // USE_LVGL_TOUCHSCREEN +#ifdef USE_LVGL_METER + +class IndicatorLine : public LvCompound { + public: + IndicatorLine() = default; + + void set_obj(lv_obj_t *lv_obj) override; + + void set_value(int value); + + private: + void update_length_(); + + int16_t angle_{}; + lv_point_precise_t points_[2]{}; +}; +#endif + #ifdef USE_LVGL_KEY_LISTENER class LVEncoderListener : public Parented { public: - LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt); + LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time); #ifdef USE_BINARY_SENSOR void add_button(binary_sensor::BinarySensor *button, lv_key_t key) { @@ -322,10 +345,10 @@ class LVEncoderListener : public Parented { } } - lv_indev_drv_t *get_drv() { return &this->drv_; } + lv_indev_t *get_drv() { return this->drv_; } protected: - lv_indev_drv_t drv_{}; + lv_indev_t *drv_{}; bool pressed_{}; int32_t count_{}; int32_t last_count_{}; @@ -336,14 +359,13 @@ class LVEncoderListener : public Parented { #ifdef USE_LVGL_LINE class LvLineType : public LvCompound { public: - std::vector get_points() { return this->points_; } - void set_points(std::vector points) { + void set_points(FixedVector points) { this->points_ = std::move(points); - lv_line_set_points(this->obj, this->points_.data(), this->points_.size()); + lv_line_set_points(this->obj, this->points_.begin(), this->points_.size()); } protected: - std::vector points_{}; + FixedVector points_{}; }; #endif #if defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER) @@ -392,7 +414,7 @@ class LvRollerType : public LvSelectable { class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound { public: void set_obj(lv_obj_t *lv_obj) override; - uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } + uint16_t get_selected() { return lv_buttonmatrix_get_selected_button(this->obj); } void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; } protected: @@ -406,5 +428,4 @@ class LvKeyboardType : public key_provider::KeyProvider, public LvCompound { void set_obj(lv_obj_t *lv_obj) override; }; #endif // USE_LVGL_KEYBOARD -} // namespace lvgl -} // namespace esphome +} // namespace esphome::lvgl diff --git a/esphome/components/lvgl/lvgl_hal.h b/esphome/components/lvgl/lvgl_hal.h deleted file mode 100644 index 754cc70391..0000000000 --- a/esphome/components/lvgl/lvgl_hal.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// Created by Clyde Stubbs on 20/9/2023. -// - -#pragma once - -#ifdef __cplusplus -#define EXTERNC extern "C" -#include -namespace esphome { -namespace lvgl {} -} // namespace esphome -#else -#define EXTERNC extern -#include -#endif - -EXTERNC size_t lv_millis(void); -EXTERNC void *lv_custom_mem_alloc(size_t size); -EXTERNC void lv_custom_mem_free(void *ptr); -EXTERNC void *lv_custom_mem_realloc(void *ptr, size_t size); diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index 98f8423b7c..c48e051eac 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -52,9 +52,9 @@ async def to_code(config): await value.get_lambda(), event_code, config[CONF_RESTORE_VALUE], - max_value=widget.get_max(), - min_value=widget.get_min(), - step=widget.get_step(), + max_value=widget.type.get_max(widget.config), + min_value=widget.type.get_min(widget.config), + step=widget.type.get_step(widget.config), ) async with LambdaContext(EVENT_ARG) as event: event.add(var.on_value()) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 2aeeedbd10..4e2bfeae85 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -29,6 +29,7 @@ from .defines import ( CONF_SCROLLBAR_MODE, CONF_TIME_FORMAT, LV_GRAD_DIR, + get_remapped_uses, ) from .helpers import CONF_IF_NAN, requires_component, validate_printf from .layout import ( @@ -42,16 +43,17 @@ from .lvcode import LvglComponent, lv_event_t_ptr from .types import ( LVEncoderListener, LvType, - WidgetType, lv_group_t, lv_obj_t, lv_pseudo_button_t, lv_style_t, ) +from .widgets import WidgetType # this will be populated later, in __init__.py to avoid circular imports. WIDGET_TYPES: dict = {} + TIME_TEXT_SCHEMA = cv.Schema( { cv.Required(CONF_TIME_FORMAT): cv.string, @@ -176,23 +178,28 @@ STYLE_PROPS = { "height": lvalid.size, "image_recolor": lvalid.lv_color, "image_recolor_opa": lvalid.opacity, - "line_width": lvalid.lv_positive_int, - "line_dash_width": lvalid.lv_positive_int, - "line_dash_gap": lvalid.lv_positive_int, - "line_rounded": lvalid.lv_bool, "line_color": lvalid.lv_color, + "line_dash_gap": lvalid.lv_positive_int, + "line_dash_width": lvalid.lv_positive_int, + "line_opa": lvalid.opacity, + "line_rounded": lvalid.lv_bool, + "line_width": lvalid.lv_positive_int, "opa": lvalid.opacity, "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, "outline_pad": lvalid.padding, "outline_width": lvalid.pixels, + "length": lvalid.pixels_or_percent, "pad_all": lvalid.padding, "pad_bottom": lvalid.padding, "pad_left": lvalid.padding, "pad_right": lvalid.padding, "pad_top": lvalid.padding, + "radial_offset": lvalid.size, "shadow_color": lvalid.lv_color, + "shadow_offset_x": lvalid.lv_int, + "shadow_offset_y": lvalid.lv_int, "shadow_ofs_x": lvalid.lv_int, "shadow_ofs_y": lvalid.lv_int, "shadow_opa": lvalid.opacity, @@ -213,7 +220,13 @@ STYLE_PROPS = { "transform_height": lvalid.pixels_or_percent, "transform_pivot_x": lvalid.pixels_or_percent, "transform_pivot_y": lvalid.pixels_or_percent, - "transform_zoom": lvalid.zoom, + "transform_rotation": lvalid.lv_angle, + "transform_scale": lvalid.scale, + "transform_scale_x": lvalid.scale, + "transform_scale_y": lvalid.scale, + "transform_skew_x": lvalid.lv_angle, + "transform_skew_y": lvalid.lv_angle, + "transform_zoom": lvalid.scale, "translate_x": lvalid.pixels_or_percent, "translate_y": lvalid.pixels_or_percent, "max_height": lvalid.pixels_or_percent, @@ -227,15 +240,23 @@ STYLE_PROPS = { } STYLE_REMAP = { - "bg_image_opa": "bg_img_opa", - "bg_image_recolor": "bg_img_recolor", - "bg_image_recolor_opa": "bg_img_recolor_opa", - "bg_image_src": "bg_img_src", - "bg_image_tiled": "bg_img_tiled", - "image_recolor": "img_recolor", - "image_recolor_opa": "img_recolor_opa", + "transform_angle": "transform_rotation", + "transform_zoom": "transform_scale", + "zoom": "scale", + "angle": "rotation", + "shadow_ofs_x": "shadow_offset_x", + "shadow_ofs_y": "shadow_offset_y", + "r_mod": "length", } + +def remap_property(prop): + if prop in STYLE_REMAP: + get_remapped_uses().add(prop) + return STYLE_REMAP[prop] + return prop + + # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { @@ -276,7 +297,7 @@ SET_STATE_SCHEMA = cv.Schema( ) # Setting object flags FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FLAGS}) -FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of) +FLAG_LIST = cv.ensure_list(df.LV_OBJ_FLAG.one_of) def part_schema(parts): @@ -418,6 +439,17 @@ ALL_STYLES = { } +def strip_defaults(schema: cv.Schema): + """ + Take a schema and remove any default values, also convert Required to Optional. + Useful for converting an object schema to a modify schema + :param schema: The original Schema + :return: A new schema with no defaults and all items optional + """ + + return cv.Schema({cv.Optional(k): v for k, v in schema.schema.items()}) + + def container_schema(widget_type: WidgetType, extras=None): """ Create a schema for a container widget of a given type. All obj properties are available, plus @@ -481,9 +513,9 @@ def any_widget_schema(extras=None): container_validator, requires_component(required) ) # Apply custom validation - value = widget_type.validate(value or {}) path = [key] if is_dict else [index, key] with prepend_path(path): + value = widget_type.validate(value or {}) result.append({key: container_validator(value)}) return result diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index ba03920a88..3b00310b67 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -6,7 +6,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" -#include "../lvgl.h" +#include "esphome/components/lvgl/lvgl_esphome.h" namespace esphome { namespace lvgl { @@ -28,12 +28,12 @@ class LVGLSelect : public select::Select, public Component { lv_obj_add_event_cb( this->widget_->obj, [](lv_event_t *e) { - auto *it = static_cast(e->user_data); + auto *it = static_cast(lv_event_get_user_data(e)); it->set_options_(); }, LV_EVENT_REFRESH, this); auto lamb = [](lv_event_t *e) { - auto *self = static_cast(e->user_data); + auto *self = static_cast(lv_event_get_user_data(e)); self->publish(); }; lv_obj_add_event_cb(this->widget_->obj, lamb, LV_EVENT_VALUE_CHANGED, this); diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index b9801b4133..6f43e78f90 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -13,7 +13,7 @@ from .defines import ( ) from .helpers import add_lv_use from .lvcode import LambdaContext, LocalVariable, lv -from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, STYLE_REMAP +from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property from .types import ObjUpdateAction, lv_obj_t, lv_style_t from .widgets import ( Widget, @@ -26,6 +26,10 @@ from .widgets import ( from .widgets.obj import obj_spec +def has_style_props(config) -> bool: + return any(prop in config for prop in ALL_STYLES) + + async def style_set(svar, style): for prop, validator in ALL_STYLES.items(): if (value := style.get(prop)) is not None: @@ -33,22 +37,44 @@ async def style_set(svar, style): value = await validator.process(value) if isinstance(value, list): value = "|".join(value) - remapped_prop = STYLE_REMAP.get(prop, prop) - lv.call(f"style_set_{remapped_prop}", svar, literal(value)) + lv.call(f"style_set_{remap_property(prop)}", svar, literal(value)) -async def create_style(style, id_name): +async def create_style(id_name, style=None): style_id = ID(id_name, True, lv_style_t) svar = cg.new_Pvariable(style_id) lv.style_init(svar) - await style_set(svar, style) + if style: + await style_set(svar, style) return svar +class LVStyle: + """ + A class to lazily create a named style + """ + + named_styles = {} + + def __init__(self, id_name, style=None): + self.id_name = id_name + self.style = style + self._style_var = None + + async def get_var(self): + if self._style_var is None: + self._style_var = await create_style(self.id_name + "_style", self.style) + return self._style_var + + @classmethod + def get_style(cls, id_name): + return cls.named_styles.setdefault(id_name, LVStyle(id_name)) + + async def styles_to_code(config): """Convert styles to C__ code.""" for style in config.get(CONF_STYLE_DEFINITIONS, ()): - await create_style(style, style[CONF_ID].id) + await create_style(style[CONF_ID].id, style) @automation.register_action( @@ -81,8 +107,7 @@ async def theme_to_code(config): for part, states in collect_parts(style).items(): styles[part] = { state: await create_style( - props, - "_lv_theme_style_" + w_name + "_" + part + "_" + state, + "_lv_theme_style_" + w_name + "_" + part + "_" + state, props ) for state, props in states.items() } @@ -90,7 +115,7 @@ async def theme_to_code(config): async def add_top_layer(lv_component, config): - top_layer = lv.disp_get_layer_top(lv_component.get_disp()) + top_layer = lv.disp_get_layer_top(lv_component.var.get_disp()) if top_conf := config.get(CONF_TOP_LAYER): with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj: top_w = Widget(top_layer_obj, obj_spec, top_conf) diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h index 4c380d69a2..eacf69b6ec 100644 --- a/esphome/components/lvgl/text/lvgl_text.h +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -1,19 +1,16 @@ #pragma once -#include - #include "esphome/components/text/text.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/preferences.h" namespace esphome { namespace lvgl { class LVGLText : public text::Text { public: - void set_control_lambda(std::function control_lambda) { - this->control_lambda_ = std::move(control_lambda); + void set_control_lambda(const std::function &control_lambda) { + this->control_lambda_ = control_lambda; if (this->initial_state_.has_value()) { this->control_lambda_(this->initial_state_.value()); this->initial_state_.reset(); @@ -28,7 +25,7 @@ class LVGLText : public text::Text { this->initial_state_ = value; } } - std::function control_lambda_{}; + std::function control_lambda_{}; optional initial_state_{}; }; diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index f2dd013f6d..0eb9f22f12 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -10,7 +10,6 @@ from .defines import ( CONF_TOUCHSCREENS, ) from .helpers import lvgl_components_required -from .lvcode import lv from .schemas import PRESS_TIME from .types import LVTouchListener @@ -40,5 +39,4 @@ async def touchscreens_to_code(lv_component, config): lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt, lv_component) - lv.indev_drv_register(listener.get_drv()) cg.add(touchscreen.register_listener(listener)) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 2f8b454ec4..c5ad4d402e 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -30,7 +30,7 @@ from .lvcode import ( lvgl_static, ) from .types import LV_EVENT -from .widgets import LvScrActType, get_scr_act, widget_map +from .widgets import LvScrActType, get_screen_active, widget_map async def add_on_boot_triggers(triggers): @@ -48,7 +48,7 @@ async def generate_triggers(): for w in widget_map.values(): if isinstance(w.type, LvScrActType): - w = get_scr_act(w.var) + w = get_screen_active(w.var) if w.config: for event, conf in { diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 09d40bb7ef..03739f3ff1 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,15 +1,9 @@ -import sys - from esphome import automation, codegen as cg -from esphome.automation import register_action -from esphome.config_validation import Schema -from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE -from esphome.core import EsphomeError -from esphome.cpp_generator import MockObj, MockObjClass +from esphome.const import CONF_TEXT, CONF_VALUE +from esphome.cpp_generator import MockObj from esphome.cpp_types import esphome_ns from .defines import lvgl_ns -from .lvcode import lv_expr class LvType(cg.MockObjClass): @@ -26,6 +20,10 @@ class LvType(cg.MockObjClass): return None return [arg[0] for arg in self.args] + @property + def name(self): + return self.base.removeprefix("lv_").removesuffix("_t") + class LvNumber(LvType): def __init__(self, *args): @@ -63,6 +61,7 @@ lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") lv_disp_t = cg.global_ns.struct("lv_disp_t") lv_color_t = cg.global_ns.struct("lv_color_t") +lv_opa_t = cg.global_ns.struct("lv_opa_t") lv_group_t = cg.global_ns.struct("lv_group_t") LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") @@ -70,10 +69,11 @@ lv_obj_t = LvType("lv_obj_t") lv_page_t = LvType("LvPageType", parents=(LvCompound,)) lv_img_t = LvType("lv_img_t") lv_gradient_t = LvType("lv_grad_dsc_t") +lv_event_t = LvType("lv_event_t") LV_EVENT = MockObj(base="LV_EVENT_", op="") LV_STATE = MockObj(base="LV_STATE_", op="") -LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="") +LV_BTNMATRIX_CTRL = MockObj(base="LV_BUTTONMATRIX_CTRL_", op="") class LvText(LvType): @@ -93,7 +93,7 @@ class LvBoolean(LvType): super().__init__( *args, largs=[(cg.bool_, "x")], - lvalue=lambda w: w.is_checked(), + lvalue=lambda w: w.has_state(LV_STATE.CHECKED), has_on_value=True, **kwargs, ) @@ -110,137 +110,3 @@ class LvSelect(LvType): parents=parens, **kwargs, ) - - -class WidgetType: - """ - Describes a type of Widget, e.g. "bar" or "line" - """ - - def __init__( - self, - name: str, - w_type: LvType, - parts: tuple, - schema=None, - modify_schema=None, - lv_name=None, - is_mock: bool = False, - ): - """ - :param name: The widget name, e.g. "bar" - :param w_type: The C type of the widget - :param parts: What parts this widget supports - :param schema: The config schema for defining a widget - :param modify_schema: A schema to update the widget, defaults to the same as the schema - :param lv_name: The name of the LVGL widget in the LVGL library, if different from the name - :param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget - """ - self.name = name - self.lv_name = lv_name or name - self.w_type = w_type - self.parts = parts - if not isinstance(schema, Schema): - schema = Schema(schema or {}) - self.schema = schema - if modify_schema is None: - modify_schema = schema - if not isinstance(modify_schema, Schema): - modify_schema = Schema(modify_schema) - self.modify_schema = modify_schema - self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") - - # Local import to avoid circular import - from .automation import update_to_code - from .schemas import WIDGET_TYPES, base_update_schema - - if not is_mock: - if self.name in WIDGET_TYPES: - raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") - WIDGET_TYPES[self.name] = self - - # Register the update action automatically, adding widget-specific properties - register_action( - f"lvgl.{self.name}.update", - ObjUpdateAction, - base_update_schema(self, self.parts).extend(self.modify_schema), - synchronous=True, - )(update_to_code) - - @property - def animated(self): - return False - - @property - def required_component(self): - return None - - def is_compound(self): - return self.w_type.inherits_from(LvCompound) - - async def to_code(self, w, config: dict): - """ - Generate code for a given widget - :param w: The widget - :param config: Its configuration - """ - - async def obj_creator(self, parent: MockObjClass, config: dict): - """ - Create an instance of the widget type - :param parent: The parent to which it should be attached - :param config: Its configuration - :return: Generated code as a single text line - """ - return lv_expr.call(f"{self.lv_name}_create", parent) - - def on_create(self, var: MockObj, config: dict): - """ - Called from to_code when the widget is created, to set up any initial properties - :param var: The variable representing the widget - :param config: Its configuration - """ - - def get_uses(self): - """ - Get a list of other widgets used by this one - :return: - """ - return () - - def get_max(self, config: dict): - return sys.maxsize - - def get_min(self, config: dict): - return -sys.maxsize - - def get_step(self, config: dict): - return 1 - - def get_scale(self, config: dict): - return 1.0 - - def validate(self, value): - """ - Provides an opportunity for custom validation for a given widget type - :param value: - :return: - """ - return value - - def final_validate(self, widget, update_config, widget_config, path): - """ - Allow final validation for a given widget type update action - :param widget: A widget - :param update_config: The configuration for the update action - :param widget_config: The configuration for the widget itself - :param path: The path to the widget, for error reporting - """ - - -class NumberType(WidgetType): - def get_max(self, config: dict): - return int(config.get(CONF_MAX_VALUE, 100)) - - def get_min(self, config: dict): - return int(config.get(CONF_MIN_VALUE, 0)) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index b1d157325b..a2a8cf2129 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -2,9 +2,18 @@ import sys from typing import Any from esphome import codegen as cg, config_validation as cv -from esphome.config_validation import Invalid -from esphome.const import CONF_DEFAULT, CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE -from esphome.core import ID, TimePeriod +from esphome.automation import register_action +from esphome.config_validation import Invalid, Schema +from esphome.const import ( + CONF_DEFAULT, + CONF_GROUP, + CONF_ID, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_STATE, + CONF_TYPE, +) +from esphome.core import ID, EsphomeError, TimePeriod from esphome.coroutine import FakeAwaitable from esphome.cpp_generator import MockObj @@ -21,6 +30,7 @@ from ..defines import ( CONF_MAIN, CONF_PAD_COLUMN, CONF_PAD_ROW, + CONF_SCALE, CONF_STYLES, CONF_WIDGETS, OBJ_FLAGS, @@ -44,8 +54,15 @@ from ..lvcode import ( lv_obj, lv_Pvariable, ) -from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES -from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr +from ..types import ( + LV_STATE, + LvCompound, + LvType, + ObjUpdateAction, + lv_coord_t, + lv_obj_t, + lv_obj_t_ptr, +) EVENT_LAMB = "event_lamb__" @@ -53,6 +70,171 @@ theme_widget_map = {} styles_used = set() +class WidgetType: + """ + Describes a type of Widget, e.g. "bar" or "line" + """ + + def __init__( + self, + name: str, + w_type: LvType, + parts: tuple, + schema=None, + modify_schema=None, + lv_name=None, + is_mock: bool = False, + ): + """ + :param name: The widget name, e.g. "bar" + :param w_type: The C type of the widget + :param parts: What parts this widget supports + :param schema: The config schema for defining a widget + :param modify_schema: A schema to update the widget, defaults to the same as the schema + :param lv_name: The name of the LVGL widget in the LVGL library, if different from the name + :param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget + """ + self.name = name + self.lv_name = lv_name or name + self.w_type = w_type + self.parts = parts + if not isinstance(schema, Schema): + schema = Schema(schema or {}) + self.schema = schema + if modify_schema is None: + modify_schema = schema + if not isinstance(modify_schema, Schema): + modify_schema = Schema(modify_schema) + self.modify_schema = modify_schema + self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") + + # Local import to avoid circular import + from ..automation import update_to_code + from ..schemas import WIDGET_TYPES, base_update_schema + + if not is_mock: + if self.name in WIDGET_TYPES: + raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") + WIDGET_TYPES[self.name] = self + + # Register the update action automatically, adding widget-specific properties + register_action( + f"lvgl.{self.name}.update", + ObjUpdateAction, + base_update_schema(self, self.parts).extend(self.modify_schema), + synchronous=True, + )(update_to_code) + + @property + def animated(self): + return False + + @property + def required_component(self): + return None + + def is_compound(self): + return self.w_type.inherits_from(LvCompound) + + async def create_to_code(self, config: dict, parent: MockObj) -> "Widget": + """ + Generate code for a widget creation. + :param config: The configuration for the widget + :param parent: The parent to which it should be attached + """ + + creator = await self.obj_creator(parent, config) + add_lv_use(self.name) + add_lv_use(*self.get_uses()) + wid = config[CONF_ID] + add_line_marks(wid) + if self.is_compound(): + var = cg.new_Pvariable(wid) + lv_add(var.set_obj(creator)) + await self.on_create(var.obj, config) + else: + var = lv_Pvariable(lv_obj_t, wid) + lv_assign(var, creator) + await self.on_create(var, config) + + w = Widget.create(wid, var, self, config) + if theme := theme_widget_map.get(self.w_type.name): + for part, states in theme.items(): + part = "LV_PART_" + part.upper() + for state, style in states.items(): + state = "LV_STATE_" + state.upper() + if state == "LV_STATE_DEFAULT": + lv_state = literal(part) + elif part == "LV_PART_MAIN": + lv_state = literal(state) + else: + lv_state = join_enums((state, part)) + w.add_style(style, lv_state) + await set_obj_properties(w, config) + await add_widgets(w, config) + await self.to_code(w, config) + return w + + async def to_code(self, w: "Widget", config: dict): + """ + Update a widget, also called when creating + :param config: + :return: + """ + + async def obj_creator(self, parent: MockObj, config: dict): + """ + Create an instance of the widget type + :param parent: The parent to which it should be attached + :param config: Its configuration + :return: Generated code as a single text line + """ + return lv_expr.call(f"{self.lv_name}_create", parent) + + async def on_create(self, var: MockObj, config: dict): + """ + Called from to_code when the widget is created, to set up any initial properties + :param var: The variable representing the widget + :param config: Its configuration + """ + + def get_uses(self): + """ + Get a list of other widgets used by this one + :return: + """ + return () + + def get_max(self, config: dict): + return sys.maxsize + + def get_min(self, config: dict): + return -sys.maxsize + + def get_step(self, config: dict): + return 1 + + def get_scale(self, config: dict): + return 1.0 + + def validate(self, value): + """ + Provides an opportunity for custom validation for a given widget type + :param value: + :return: + """ + return value + + def final_validate(self, widget, update_config, widget_config, path): + """ + Allow final validation for a given widget type update action + :param widget: A widget + :param update_config: The configuration for the update action + :param widget_config: The configuration for the widget itself + :param path: The path to the widget, for error reporting + """ + + class Widget: """ Represents a Widget. @@ -74,19 +256,25 @@ class Widget: self.obj = var self.outer = None self.move_to_foreground = False + # Properties for linear equations + self.slope = None + self.y_int = None @staticmethod def create(name, var, wtype: WidgetType, config: dict = None): w = Widget(var, wtype, config) - if name is not None: - widget_map[name] = w + widget_map[name] = w return w def add_state(self, state): + if "|" in state: + state = f"(lv_state_t)({state})" return lv_obj.add_state(self.obj, literal(state)) def clear_state(self, state): - return lv_obj.clear_state(self.obj, literal(state)) + if "|" in state: + state = f"(lv_state_t)({state})" + return lv_obj.remove_state(self.obj, literal(state)) def has_state(self, state): return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0 @@ -98,12 +286,23 @@ class Widget: return self.has_state(LV_STATE.CHECKED) def add_flag(self, flag): + if "|" in flag: + flag = f"(lv_obj_flag_t)({flag})" return lv_obj.add_flag(self.obj, literal(flag)) def clear_flag(self, flag): - return lv_obj.clear_flag(self.obj, literal(flag)) + if "|" in flag: + flag = f"(lv_obj_flag_t)({flag})" + return lv_obj.remove_flag(self.obj, literal(flag)) - async def set_property(self, prop, value, animated: bool = None, lv_name=None): + def add_style(self, style_id, state=LV_STATE.DEFAULT): + if "|" in state: + state = f"(lv_state_t)({state})" + lv_obj.add_style(self.obj, MockObj(style_id), literal(state)) + + async def set_property( + self, prop, value, animated: bool = None, lv_name=None, processor=None + ): """ Set a property of the widget. :param prop: The property name @@ -111,18 +310,28 @@ class Widget: :param animated: If the change should be animated :param lv_name: The base type of the widget e.g. "obj" """ + + from ..schemas import ALL_STYLES, remap_property + if isinstance(value, dict): value = value.get(prop) - if isinstance(ALL_STYLES.get(prop), LValidator): - value = await ALL_STYLES[prop].process(value) - else: - value = literal(value) - if value is None: + if value is None: + return + if not processor and isinstance(ALL_STYLES.get(prop), LValidator): + processor = ALL_STYLES[prop] + if isinstance(processor, LValidator): + processor = processor.process + if processor: + value = await processor(value) + elif value is None: return + prop = remap_property(prop) if isinstance(value, TimePeriod): value = value.total_milliseconds - if isinstance(value, str): + elif isinstance(value, str): value = literal(value) + elif isinstance(value, ID): + value = MockObj(value) lv_name = lv_name or self.type.lv_name if animated is None or self.type.animated is not True: lv.call(f"{lv_name}_set_{prop}", self.obj, value) @@ -138,10 +347,12 @@ class Widget: ltype = ltype or self.__type_base() return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})") - def set_style(self, prop, value, state): + def set_style(self, prop, value, state=LV_STATE.DEFAULT): if value is None: return styles_used.add(prop) + if isinstance(value, str): + value = literal(value) lv.call(f"obj_set_style_{prop}", self.obj, value, state) def __type_base(self): @@ -189,15 +400,6 @@ class Widget: """ return - def get_max(self): - return self.type.get_max(self.config) - - def get_min(self): - return self.type.get_min(self.config) - - def get_step(self): - return self.type.get_step(self.config) - def get_scale(self): return self.type.get_scale(self.config) @@ -212,14 +414,14 @@ class LvScrActType(WidgetType): """ def __init__(self): - super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True) + super().__init__("lv_screen_active()", lv_obj_t, (), is_mock=True) async def to_code(self, w, config: dict): - return [] + pass -def get_scr_act(lv_comp: MockObj) -> Widget: - return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {}) +def get_screen_active(lv_comp: MockObj) -> Widget: + return Widget(lv_comp.get_screen_active(), LvScrActType(), {}) def get_widget_generator(wid): @@ -271,10 +473,17 @@ def collect_props(config): :param config: :return: """ + + from ..schemas import ALL_STYLES + props = {} for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]: if prop in config: - props[prop] = config[prop] + if prop == CONF_SCALE: + props[CONF_SCALE + "_x"] = config[prop] + props[CONF_SCALE + "_y"] = config[prop] + else: + props[prop] = config[prop] return props @@ -304,34 +513,41 @@ def collect_parts(config): return parts +def _size_to_str(value): + if isinstance(value, float): + return f"lv_pct({int(value * 100)})" + return str(value) + + async def set_obj_properties(w: Widget, config): """Generate a list of C++ statements to apply properties to an lv_obj_t""" + + from ..schemas import ALL_STYLES, OBJ_PROPERTIES, remap_property + if layout := config.get(CONF_LAYOUT): layout_type: str = layout[CONF_TYPE] add_lv_use(layout_type) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) if (pad_row := layout.get(CONF_PAD_ROW)) is not None: - w.set_style(CONF_PAD_ROW, pad_row, 0) + w.set_style(CONF_PAD_ROW, pad_row) if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: - w.set_style(CONF_PAD_COLUMN, pad_column, 0) + w.set_style(CONF_PAD_COLUMN, pad_column) if layout_type == TYPE_GRID: wid = config[CONF_ID] - rows = [str(x) for x in layout[CONF_GRID_ROWS]] + rows = [_size_to_str(x) for x in layout[CONF_GRID_ROWS]] rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}" row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) - w.set_style("grid_row_dsc_array", row_array, 0) - columns = [str(x) for x in layout[CONF_GRID_COLUMNS]] + w.set_style("grid_row_dsc_array", row_array) + columns = [_size_to_str(x) for x in layout[CONF_GRID_COLUMNS]] columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}" column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) - w.set_style("grid_column_dsc_array", column_array, 0) + w.set_style("grid_column_dsc_array", column_array) w.set_style( - CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0 - ) - w.set_style( - CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0 + CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)) ) + w.set_style(CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN))) if layout_type == TYPE_FLEX: lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) main = literal(layout[CONF_FLEX_ALIGN_MAIN]) @@ -353,13 +569,13 @@ async def set_obj_properties(w: Widget, config): else: lv_state = join_enums((state, part)) for style_id in props.get(CONF_STYLES, ()): - lv_obj.add_style(w.obj, MockObj(style_id), lv_state) + w.add_style(style_id, lv_state) for prop, value in { k: v for k, v in props.items() if k in ALL_STYLES }.items(): if isinstance(ALL_STYLES[prop], LValidator): value = await ALL_STYLES[prop].process(value) - prop_r = STYLE_REMAP.get(prop, prop) + prop_r = remap_property(prop) w.set_style(prop_r, value, lv_state) if group := config.get(CONF_GROUP): group = await cg.get_variable(group) @@ -429,7 +645,7 @@ async def add_widgets(parent: Widget, config: dict): await widget_to_code(w_cnfig, w_type, parent.obj) -async def widget_to_code(w_cnfig, w_type: WidgetType, parent): +async def widget_to_code(w_cnfig, w_type: WidgetType | str, parent) -> Widget: """ Converts a Widget definition to C code. :param w_cnfig: The widget configuration @@ -437,34 +653,18 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): :param parent: The parent to which the widget should be added :return: """ - spec: WidgetType = WIDGET_TYPES[w_type] - creator = await spec.obj_creator(parent, w_cnfig) - add_lv_use(spec.name) - add_lv_use(*spec.get_uses()) - wid = w_cnfig[CONF_ID] - add_line_marks(wid) - if spec.is_compound(): - var = cg.new_Pvariable(wid) - lv_add(var.set_obj(creator)) - spec.on_create(var.obj, w_cnfig) - else: - var = lv_Pvariable(lv_obj_t, wid) - lv_assign(var, creator) - spec.on_create(var, w_cnfig) - w = Widget.create(wid, var, spec, w_cnfig) - if theme := theme_widget_map.get(w_type): - for part, states in theme.items(): - part = "LV_PART_" + part.upper() - for state, style in states.items(): - state = "LV_STATE_" + state.upper() - if state == "LV_STATE_DEFAULT": - lv_state = literal(part) - elif part == "LV_PART_MAIN": - lv_state = literal(state) - else: - lv_state = join_enums((state, part)) - lv.obj_add_style(w.obj, style, lv_state) - await set_obj_properties(w, w_cnfig) - await add_widgets(w, w_cnfig) - await spec.to_code(w, w_cnfig) + from ..schemas import WIDGET_TYPES + + spec: WidgetType = ( + w_type if isinstance(w_type, WidgetType) else WIDGET_TYPES[w_type] + ) + return await spec.create_to_code(w_cnfig, parent) + + +class NumberType(WidgetType): + def get_max(self, config: dict): + return int(config.get(CONF_MAX_VALUE, 100)) + + def get_min(self, config: dict): + return int(config.get(CONF_MIN_VALUE, 0)) diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index 34ac9c51f7..9eaf3dadce 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -18,7 +18,8 @@ from ..defines import ( CONF_KNOB, CONF_MAIN, CONF_START_ANGLE, - literal, + LV_OBJ_FLAG, + LV_PART, ) from ..lv_validation import ( get_start_value, @@ -28,8 +29,8 @@ from ..lv_validation import ( lv_positive_int, ) from ..lvcode import lv, lv_expr, lv_obj -from ..types import LvNumber, NumberType -from . import Widget +from ..types import LvNumber +from . import NumberType, Widget CONF_ARC = "arc" ARC_SCHEMA = cv.Schema( @@ -71,39 +72,17 @@ class ArcType(NumberType): ) async def to_code(self, w: Widget, config): - if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: - max_value = await lv_int.process(config[CONF_MAX_VALUE]) - min_value = await lv_int.process(config[CONF_MIN_VALUE]) - lv.arc_set_range(w.obj, min_value, max_value) - elif CONF_MIN_VALUE in config: - max_value = w.get_property(CONF_MAX_VALUE) - min_value = await lv_int.process(config[CONF_MIN_VALUE]) - lv.arc_set_range(w.obj, min_value, max_value) - elif CONF_MAX_VALUE in config: - max_value = await lv_int.process(config[CONF_MAX_VALUE]) - min_value = w.get_property(CONF_MIN_VALUE) - lv.arc_set_range(w.obj, min_value, max_value) - - await w.set_property( - "bg_start_angle", - await lv_angle_degrees.process(config.get(CONF_START_ANGLE)), - ) - await w.set_property( - "bg_end_angle", await lv_angle_degrees.process(config.get(CONF_END_ANGLE)) - ) - await w.set_property( - CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION)) - ) - await w.set_property(CONF_MODE, config) - await w.set_property( - CONF_CHANGE_RATE, - await lv_positive_int.process(config.get(CONF_CHANGE_RATE)), - ) - + for prop, validator in ARC_MODIFY_SCHEMA.schema.items(): + if prop != CONF_VALUE: + # start_angle and end_angle are mapped to bg_start_angle and bg_end_angle + prop = str(prop) + if prop.endswith("_angle"): + prop = "bg_" + prop + await w.set_property(prop, config, processor=validator) if CONF_ADJUSTABLE in config: if not config[CONF_ADJUSTABLE]: - lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) - w.clear_flag("LV_OBJ_FLAG_CLICKABLE") + lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB) + w.clear_flag(LV_OBJ_FLAG.CLICKABLE) elif CONF_GROUP not in config: # For some reason arc does not get automatically added to the default group lv.group_add_obj(lv_expr.group_get_default(), w.obj) diff --git a/esphome/components/lvgl/widgets/button.py b/esphome/components/lvgl/widgets/button.py index 5f2910174f..b943a4d9aa 100644 --- a/esphome/components/lvgl/widgets/button.py +++ b/esphome/components/lvgl/widgets/button.py @@ -7,11 +7,11 @@ from ..helpers import add_lv_use from ..lv_validation import lv_text from ..lvcode import lv, lv_expr from ..schemas import TEXT_SCHEMA -from ..types import LvBoolean, WidgetType -from . import Widget +from ..types import LvBoolean +from . import Widget, WidgetType from .label import label_spec -lv_button_t = LvBoolean("lv_btn_t") +lv_button_t = LvBoolean("lv_button_t") class ButtonType(WidgetType): @@ -30,7 +30,7 @@ class ButtonType(WidgetType): def get_uses(self): return ("btn",) - def on_create(self, var: MockObj, config: dict): + async def on_create(self, var: MockObj, config: dict): if CONF_TEXT in config: lv.label_create(var) return var diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index f94f12b69b..f5ae0deba9 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -118,15 +118,15 @@ class MatrixButton(Widget): def has_state(self, state): state = self.map_ctrls(state) - return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state) + return lv_expr.buttonmatrix_has_button_ctrl(self.obj, self.index, state) def add_state(self, state): state = self.map_ctrls(state) - return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state) + return lv.buttonmatrix_set_button_ctrl(self.obj, self.index, state) def clear_state(self, state): state = self.map_ctrls(state) - return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state) + return lv.buttonmatrix_clear_button_ctrl(self.obj, self.index, state) def is_pressed(self): return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED) @@ -161,7 +161,7 @@ async def get_button_data(config, buttonmatrix: Widget): text_list.append(button_conf.get(CONF_TEXT) or "") key_list.append(button_conf.get(CONF_KEY_CODE) or 0) width_list.append(button_conf[CONF_WIDTH]) - ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"] + ctrl = ["CLICK_TRIG"] for item in button_conf.get(CONF_CONTROL, ()): ctrl.extend([k for k, v in item.items() if v]) ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl)) @@ -187,7 +187,7 @@ class ButtonMatrixType(WidgetType): (CONF_MAIN, CONF_ITEMS), BUTTONMATRIX_SCHEMA, {}, - lv_name="btnmatrix", + lv_name="buttonmatrix", ) async def to_code(self, w: Widget, config): @@ -199,22 +199,22 @@ class ButtonMatrixType(WidgetType): ) text_id = config[CONF_BUTTON_TEXT_LIST_ID] text_id = cg.static_const_array(text_id, text_list) - lv.btnmatrix_set_map(w.obj, text_id) + lv.buttonmatrix_set_map(w.obj, text_id) set_btn_data(w.obj, ctrl_list, width_list) - lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED]) + lv.buttonmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED]) for index, key in enumerate(key_list): if key != 0: lv_add(w.var.set_key(index, key)) def get_uses(self): - return ("btnmatrix",) + return ("buttonmatrix",) def set_btn_data(obj, ctrl_list, width_list): for index, ctrl in enumerate(ctrl_list): - lv.btnmatrix_set_btn_ctrl(obj, index, ctrl) + lv.buttonmatrix_set_button_ctrl(obj, index, ctrl) for index, width in enumerate(width_list): - lv.btnmatrix_set_btn_width(obj, index, width) + lv.buttonmatrix_set_button_width(obj, index, width) buttonmatrix_spec = ButtonMatrixType() @@ -253,25 +253,21 @@ async def button_update_to_code(config, action_id, template_arg, args): async def do_button_update(w): if (width := config.get(CONF_WIDTH)) is not None: - lv.btnmatrix_set_btn_width(w.obj, w.index, width) + lv.buttonmatrix_set_button_width(w.obj, w.index, width) if config.get(CONF_SELECTED): - lv.btnmatrix_set_selected_btn(w.obj, w.index) + lv.buttonmatrix_set_selected_button(w.obj, w.index) if controls := config.get(CONF_CONTROL): adds = [] clrs = [] for item in controls: - adds.extend( - [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v] - ) - clrs.extend( - [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v] - ) + adds.extend([f"{k.upper()}" for k, v in item.items() if v]) + clrs.extend([f"{k.upper()}" for k, v in item.items() if not v]) if adds: - lv.btnmatrix_set_btn_ctrl( + lv.buttonmatrix_set_button_ctrl( w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds) ) if clrs: - lv.btnmatrix_clear_btn_ctrl( + lv.buttonmatrix_clear_button_ctrl( w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs) ) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 50cc8b0af6..c670e3732c 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -1,3 +1,22 @@ +""" +LVGL 9.4 Canvas Widget Implementation + +This module implements the canvas widget for LVGL 9.4. Key changes from LVGL 8.4: + +1. Buffer allocation: + - LV_IMG_CF_TRUE_COLOR → LV_COLOR_FORMAT_RGB565 + - LV_IMG_CF_TRUE_COLOR_ALPHA → LV_COLOR_FORMAT_ARGB8888 + - LV_CANVAS_BUF_SIZE_TRUE_COLOR → LV_CANVAS_BUF_SIZE(w, h, bpp, stride) + +2. Drawing API: + - All lv_canvas_draw_* functions removed + - Use layer-based drawing: lv_canvas_init_layer() / lv_canvas_finish_layer() + - Draw using low-level lv_draw_* functions (rect, line, arc, image, label) + +3. Pixel operations: + - lv_canvas_set_px_color + lv_canvas_set_px_opa → lv_canvas_set_px(color, opa) +""" + from esphome import automation, codegen as cg, config_validation as cv from esphome.components.display_menu_base import CONF_LABEL from esphome.const import ( @@ -9,7 +28,7 @@ from esphome.const import ( CONF_X, CONF_Y, ) -from esphome.cpp_generator import Literal, MockObj +from esphome.cpp_types import FixedVector from ..automation import action_to_code from ..defines import ( @@ -19,8 +38,11 @@ from ..defines import ( CONF_PIVOT_X, CONF_PIVOT_Y, CONF_POINTS, + CONF_RADIUS, CONF_SRC, CONF_START_ANGLE, + addr, + get_color_formats, literal, ) from ..lv_validation import ( @@ -34,18 +56,20 @@ from ..lv_validation import ( size, ) from ..lvcode import LocalVariable, lv, lv_assign, lv_expr -from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema -from ..types import LvType, ObjUpdateAction, WidgetType -from . import Widget, get_widgets -from .line import lv_point_t, process_coord +from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property +from ..types import LvType, ObjUpdateAction +from . import Widget, WidgetType, get_widgets +from .img import CONF_IMAGE +from .line import lv_point_precise_t, process_coord CONF_CANVAS = "canvas" CONF_BUFFER_ID = "buffer_id" CONF_MAX_WIDTH = "max_width" CONF_TRANSPARENT = "transparent" -CONF_RADIUS = "radius" +CONF_DRAW_BUF_ID = "draw_buf_id" lv_canvas_t = LvType("lv_canvas_t") +lv_draw_buf_t = LvType("lv_draw_buf_t") class CanvasType(WidgetType): @@ -59,32 +83,44 @@ class CanvasType(WidgetType): cv.Required(CONF_WIDTH): size, cv.Required(CONF_HEIGHT): size, cv.Optional(CONF_TRANSPARENT, default=False): cv.boolean, + cv.GenerateID(CONF_DRAW_BUF_ID): cv.declare_id(lv_draw_buf_t), } ), + modify_schema={}, ) def get_uses(self): - return "img", CONF_LABEL + return CONF_IMAGE, CONF_LABEL async def to_code(self, w: Widget, config): width = config[CONF_WIDTH] height = config[CONF_HEIGHT] - use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else "" - buf_size = literal( - f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})" + # LVGL 9.4: Use LV_COLOR_FORMAT instead of LV_IMG_CF + # RGB565 is 16-bit (2 bytes per pixel), ARGB8888 is 32-bit (4 bytes per pixel) + if config[CONF_TRANSPARENT]: + color_format = "LV_COLOR_FORMAT_ARGB8888" + get_color_formats().add("ARGB8888") + else: + color_format = "LV_COLOR_FORMAT_NATIVE" + + # LVGL 9.4: LV_CANVAS_BUF_SIZE(width, height, bits_per_pixel, stride) + # stride is 0 for default (width * bytes_per_pixel) + draw_buf = cg.new_Pvariable(config[CONF_DRAW_BUF_ID]) + buf_size = literal(f"LV_DRAW_BUF_SIZE({width}, {height}, {color_format})") + lv.draw_buf_init( + draw_buf, + width, + height, + literal(color_format), + 0, + lv_expr.malloc_core(buf_size), + literal(buf_size), ) - with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf: - cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});")) - lv.canvas_set_buffer( - w.obj, - buf, - width, - height, - literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), - ) + lv.draw_buf_set_flag(draw_buf, literal("LV_IMAGE_FLAGS_MODIFIABLE")) + lv.canvas_set_draw_buf(w.obj, draw_buf) -canvas_spec = CanvasType() +CanvasType() @automation.register_action( @@ -117,7 +153,7 @@ async def canvas_fill(config, action_id, template_arg, args): { cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), cv.Required(CONF_COLOR): lv_color, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_OPA, default="COVER"): opacity, cv.Required(CONF_POINTS): cv.ensure_list(point_schema), }, ), @@ -126,7 +162,7 @@ async def canvas_fill(config, action_id, template_arg, args): async def canvas_set_pixel(config, action_id, template_arg, args): widget = await get_widgets(config) color = await lv_color.process(config[CONF_COLOR]) - opa = await opacity.process(config.get(CONF_OPA)) + opa = await opacity.process(config.get(CONF_OPA), "COVER") points = [ ( await pixels.process(p[CONF_X]), @@ -136,25 +172,11 @@ async def canvas_set_pixel(config, action_id, template_arg, args): ] async def do_set_pixels(w: Widget): - if isinstance(color, MockObj): - for point in points: - x, y = point - lv.canvas_set_px_color(w.obj, x, y, color) - else: - with LocalVariable("color", "lv_color_t", color, modifier="") as color_var: - for point in points: - x, y = point - lv.canvas_set_px_color(w.obj, x, y, color_var) - if opa: - if isinstance(opa, Literal): - for point in points: - x, y = point - lv.canvas_set_px_opa(w.obj, x, y, opa) - else: - with LocalVariable("opa", "lv_opa_t", opa, modifier="") as opa_var: - for point in points: - x, y = point - lv.canvas_set_px_opa(w.obj, x, y, opa_var) + # LVGL 9.4: lv_canvas_set_px combines color and opacity + # Could optimize this for lambda values + for point in points: + x, y = point + lv.canvas_set_px(w.obj, x, y, color, opa) return await action_to_code( widget, do_set_pixels, action_id, template_arg, args, config @@ -178,18 +200,21 @@ async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg y = await pixels.process(config.get(CONF_Y)) async def action_func(w: Widget): - with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc: - dsc_addr = literal(f"&{dsc}") - lv.call(f"draw_{dsc_type}_dsc_init", dsc_addr) - if CONF_OPA in config: - opa = await opacity.process(config[CONF_OPA]) - lv_assign(dsc.opa, opa) - for prop, validator in props.items(): - if prop in config: - value = await validator.process(config[prop]) - mapped_prop = STYLE_REMAP.get(prop, prop) - lv_assign(getattr(dsc, mapped_prop), value) - await do_draw(w, x, y, dsc_addr) + # LVGL 9.4: Create a layer for drawing on canvas + with LocalVariable("layer", "lv_layer_t", modifier="") as layer: + lv.canvas_init_layer(w.obj, addr(layer)) + with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc: + lv.call(f"draw_{dsc_type}_dsc_init", addr(dsc)) + if CONF_OPA in config: + opa = await opacity.process(config[CONF_OPA]) + lv_assign(dsc.opa, opa) + for prop, validator in props.items(): + if prop in config: + value = await validator.process(config[prop]) + mapped_prop = remap_property(prop) + lv_assign(getattr(dsc, mapped_prop), value) + await do_draw(addr(layer), x, y, dsc) + lv.canvas_finish_layer(w.obj, addr(layer)) return await action_to_code( widget, action_func, action_id, template_arg, args, config @@ -212,6 +237,8 @@ RECT_PROPS = { "outline_opa", "shadow_color", "shadow_width", + "shadow_offset_x", + "shadow_offset_y", "shadow_ofs_x", "shadow_ofs_y", "shadow_spread", @@ -220,6 +247,24 @@ RECT_PROPS = { } +def _draw_line(layer, dsc, points): + # LVGL 9.4: Use lv_draw_line for each line segment + with ( + LocalVariable( + "points", FixedVector.template(lv_point_precise_t), points, modifier="" + ) as points_var, + LocalVariable("i", "uint32_t", literal("0"), modifier="") as i, + ): + # Draw lines between consecutive points + lv.append( + cg.RawStatement(f"for ({i} = 0; {i} != {points_var}.size() - 1; {i}++) {{") + ) + lv_assign(dsc.p1, points_var[i]) + lv_assign(dsc.p2, points_var[i + 1]) + lv.draw_line(layer, addr(dsc)) + lv.append(cg.RawStatement("}")) + + @automation.register_action( "lvgl.canvas.draw_rectangle", ObjUpdateAction, @@ -237,8 +282,14 @@ async def canvas_draw_rect(config, action_id, template_arg, args): width = await pixels.process(config[CONF_WIDTH]) height = await pixels.process(config[CONF_HEIGHT]) - async def do_draw_rect(w: Widget, x, y, dsc_addr): - lv.canvas_draw_rect(w.obj, x, y, width, height, dsc_addr) + async def do_draw_rect(layer, x, y, dsc): + # LVGL 9.4: Use lv_draw_rect with area + with LocalVariable("area", "lv_area_t", modifier="") as area: + lv_assign(area.x1, x) + lv_assign(area.y1, y) + lv_assign(area.x2, literal(f"{x} + {width} - 1")) + lv_assign(area.y2, literal(f"{y} + {height} - 1")) + lv.draw_rect(layer, addr(dsc), addr(area)) return await draw_to_code( config, "rect", RECT_PROPS, do_draw_rect, action_id, template_arg, args @@ -277,21 +328,55 @@ async def canvas_draw_text(config, action_id, template_arg, args): text = await lv_text.process(config[CONF_TEXT]) max_width = await pixels.process(config[CONF_MAX_WIDTH]) - async def do_draw_text(w: Widget, x, y, dsc_addr): - lv.canvas_draw_text(w.obj, x, y, max_width, dsc_addr, text) + async def do_draw_text(layer, x, y, dsc): + # LVGL 9.4: Use lv_draw_label with area and hint + with LocalVariable("area", "lv_area_t", modifier="") as area: + lv_assign(area.x1, x) + lv_assign(area.y1, y) + lv_assign(area.x2, literal(f"{x} + {max_width} - 1")) + lv_assign(area.y2, literal(f"{y} + LV_COORD_MAX")) + lv_assign(dsc.text, text) + lv.draw_label(layer, addr(dsc), addr(area)) return await draw_to_code( config, "label", TEXT_PROPS, do_draw_text, action_id, template_arg, args ) -IMG_PROPS = { - "angle": STYLE_PROPS["transform_angle"], - "zoom": STYLE_PROPS["transform_zoom"], - "recolor": STYLE_PROPS["image_recolor"], - "recolor_opa": STYLE_PROPS["image_recolor_opa"], - "opa": STYLE_PROPS["opa"], -} +IMG_PROPS = ( + "angle", + "rotation", + "scale_x", + "scale_y", + "skew_x", + "skew_y", + "scale", + "zoom", + "recolor", + "recolor_opa", + "opa", +) + + +def _scale_map(config): + config = {remap_property(p): v for p, v in config.items()} + if "scale" in config and {"scale_x", "scale_y"} & config.keys(): + raise cv.Invalid("Cannot specify both scale and scale_x/scale_y") + if "scale" in config: + config.update({"scale_x": config["scale"], "scale_y": config["scale"]}) + del config["scale"] + return config + + +def _get_prop_validator(prop): + return STYLE_PROPS.get(f"transform_{remap_property(prop)}") or STYLE_PROPS.get(prop) + + +def _prop_validator(prop): + def validator(value): + return _get_prop_validator(prop)(value) + + return validator @automation.register_action( @@ -303,9 +388,9 @@ IMG_PROPS = { cv.Required(CONF_SRC): lv_image, cv.Optional(CONF_PIVOT_X, default=0): pixels, cv.Optional(CONF_PIVOT_Y, default=0): pixels, - **{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}, + **{cv.Optional(prop): _prop_validator(prop) for prop in IMG_PROPS}, } - ), + ).add_extra(_scale_map), synchronous=True, ) async def canvas_draw_image(config, action_id, template_arg, args): @@ -313,15 +398,29 @@ async def canvas_draw_image(config, action_id, template_arg, args): pivot_x = await pixels.process(config[CONF_PIVOT_X]) pivot_y = await pixels.process(config[CONF_PIVOT_Y]) - async def do_draw_image(w: Widget, x, y, dsc_addr): - dsc = MockObj(f"(*{dsc_addr})") + async def do_draw_image(layer, x, y, dsc): + # LVGL 9.4: Use lv_draw_image with area + lv_assign(dsc.src, src.get_lv_image_dsc()) if pivot_x or pivot_y: # pylint :disable=no-member - lv_assign(dsc.pivot, literal(f"{{{pivot_x}, {pivot_y}}}")) - lv.canvas_draw_img(w.obj, x, y, src, dsc_addr) + lv_assign(dsc.pivot.x, pivot_x) + lv_assign(dsc.pivot.y, pivot_y) + with LocalVariable("area", "lv_area_t", modifier="") as area: + lv_assign(area.x1, x) + lv_assign(area.y1, y) + # Image size will be determined from the image descriptor + lv_assign(area.x2, x) + lv_assign(area.y2, y) + lv.draw_image(layer, addr(dsc), addr(area)) return await draw_to_code( - config, "img", IMG_PROPS, do_draw_image, action_id, template_arg, args + config, + "image", + {prop: _get_prop_validator(prop) for prop in IMG_PROPS}, + do_draw_image, + action_id, + template_arg, + args, ) @@ -354,11 +453,8 @@ async def canvas_draw_line(config, action_id, template_arg, args): for p in config[CONF_POINTS] ] - async def do_draw_line(w: Widget, x, y, dsc_addr): - with LocalVariable( - "points", cg.std_vector.template(lv_point_t), points, modifier="" - ) as points_var: - lv.canvas_draw_line(w.obj, points_var.data(), points_var.size(), dsc_addr) + async def do_draw_line(layer, _x, _y, dsc): + _draw_line(layer, dsc, points) return await draw_to_code( config, "line", LINE_PROPS, do_draw_line, action_id, template_arg, args @@ -382,14 +478,20 @@ async def canvas_draw_polygon(config, action_id, template_arg, args): [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] for p in config[CONF_POINTS] ] + # Close the polygon + points.append(points[0]) - async def do_draw_polygon(w: Widget, x, y, dsc_addr): - with LocalVariable( - "points", cg.std_vector.template(lv_point_t), points, modifier="" - ) as points_var: - lv.canvas_draw_polygon( - w.obj, points_var.data(), points_var.size(), dsc_addr - ) + async def do_draw_polygon(layer, x, y, dsc): + # LVGL 9.4: Draw polygon using line drawing in a closed path + # Note: This draws outline only. For filled polygons, would need different approach + # Convert rect descriptor to line descriptor for polygon outline + with LocalVariable("line_dsc", "lv_draw_line_dsc_t", modifier="") as line_dsc: + lv.draw_line_dsc_init(addr(line_dsc)) + # Copy border properties from rect descriptor to line descriptor + lv_assign(line_dsc.color, dsc.border_color) + lv_assign(line_dsc.width, dsc.border_width) + lv_assign(line_dsc.opa, dsc.border_opa) + _draw_line(layer, line_dsc, points) return await draw_to_code( config, "rect", RECT_PROPS, do_draw_polygon, action_id, template_arg, args @@ -422,8 +524,14 @@ async def canvas_draw_arc(config, action_id, template_arg, args): start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE]) end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE]) - async def do_draw_arc(w: Widget, x, y, dsc_addr): - lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr) + async def do_draw_arc(layer, x, y, dsc): + # LVGL 9.4: Use lv_draw_arc with center point + lv_assign(dsc.center.x, x) + lv_assign(dsc.center.y, y) + lv_assign(dsc.start_angle, start_angle) + lv_assign(dsc.end_angle, end_angle) + lv_assign(dsc.radius, radius) + lv.draw_arc(layer, addr(dsc)) return await draw_to_code( config, "arc", ARC_PROPS, do_draw_arc, action_id, template_arg, args diff --git a/esphome/components/lvgl/widgets/container.py b/esphome/components/lvgl/widgets/container.py index 2ac1a3b244..427b46affa 100644 --- a/esphome/components/lvgl/widgets/container.py +++ b/esphome/components/lvgl/widgets/container.py @@ -1,11 +1,10 @@ import esphome.config_validation as cv from esphome.const import CONF_HEIGHT, CONF_WIDTH -from esphome.cpp_generator import MockObj -from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR +from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_SCROLLBAR from ..lv_validation import size -from ..lvcode import lv -from ..types import WidgetType, lv_obj_t +from ..types import lv_obj_t +from . import WidgetType CONTAINER_SCHEMA = cv.Schema( { @@ -28,12 +27,9 @@ class ContainerType(WidgetType): (CONF_MAIN, CONF_SCROLLBAR), schema=CONTAINER_SCHEMA, modify_schema={}, - lv_name=CONF_OBJ, + lv_name=CONF_CONTAINER, ) self.styles = {} - def on_create(self, var: MockObj, config: dict): - lv.obj_remove_style_all(var) - container_spec = ContainerType() diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index 8ec18e3033..ed6fd30c09 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -1,17 +1,22 @@ import esphome.config_validation as cv -from esphome.const import CONF_ANGLE, CONF_MODE, CONF_OFFSET_X, CONF_OFFSET_Y +from esphome.const import ( + CONF_ANGLE, + CONF_MODE, + CONF_OFFSET_X, + CONF_OFFSET_Y, + CONF_ROTATION, +) from ..defines import ( CONF_ANTIALIAS, CONF_MAIN, CONF_PIVOT_X, CONF_PIVOT_Y, + CONF_SCALE, CONF_SRC, CONF_ZOOM, - LvConstant, ) -from ..lv_validation import lv_angle, lv_bool, lv_image, size, zoom -from ..lvcode import lv +from ..lv_validation import lv_angle, lv_bool, lv_image, scale, size from ..types import lv_img_t from . import Widget, WidgetType from .label import CONF_LABEL @@ -22,14 +27,14 @@ BASE_IMG_SCHEMA = cv.Schema( { cv.Optional(CONF_PIVOT_X): size, cv.Optional(CONF_PIVOT_Y): size, - cv.Optional(CONF_ANGLE): lv_angle, - cv.Optional(CONF_ZOOM): zoom, + cv.Exclusive(CONF_ANGLE, CONF_ROTATION): lv_angle, + cv.Exclusive(CONF_ROTATION, CONF_ROTATION): lv_angle, + cv.Exclusive(CONF_ZOOM, CONF_SCALE): scale, + cv.Exclusive(CONF_SCALE, CONF_SCALE): scale, cv.Optional(CONF_OFFSET_X): size, cv.Optional(CONF_OFFSET_Y): size, cv.Optional(CONF_ANTIALIAS): lv_bool, - cv.Optional(CONF_MODE): LvConstant( - "LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL" - ).one_of, + cv.Optional(CONF_MODE): cv.invalid(f"{CONF_MODE} is not supported in LVGL 9.x"), } ) @@ -54,33 +59,15 @@ class ImgType(WidgetType): (CONF_MAIN,), IMG_SCHEMA, IMG_MODIFY_SCHEMA, - lv_name="img", ) def get_uses(self): - return "img", CONF_LABEL + return CONF_IMAGE, CONF_LABEL async def to_code(self, w: Widget, config): - if src := config.get(CONF_SRC): - lv.img_set_src(w.obj, await lv_image.process(src)) - if (pivot_x := config.get(CONF_PIVOT_X)) and ( - pivot_y := config.get(CONF_PIVOT_Y) - ): - lv.img_set_pivot( - w.obj, await size.process(pivot_x), await size.process(pivot_y) - ) - if (cf_angle := config.get(CONF_ANGLE)) is not None: - lv.img_set_angle(w.obj, await lv_angle.process(cf_angle)) - if (img_zoom := config.get(CONF_ZOOM)) is not None: - lv.img_set_zoom(w.obj, await zoom.process(img_zoom)) - if (offset := config.get(CONF_OFFSET_X)) is not None: - lv.img_set_offset_x(w.obj, await size.process(offset)) - if (offset := config.get(CONF_OFFSET_Y)) is not None: - lv.img_set_offset_y(w.obj, await size.process(offset)) - if CONF_ANTIALIAS in config: - lv.img_set_antialias(w.obj, await lv_bool.process(config[CONF_ANTIALIAS])) - if mode := config.get(CONF_MODE): - await w.set_property("size_mode", mode) + await w.set_property(CONF_SRC, await lv_image.process(config.get(CONF_SRC))) + for prop, validator in BASE_IMG_SCHEMA.schema.items(): + await w.set_property(prop, config, processor=validator) img_spec = ImgType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index 8afd8d610f..bb5900b8c9 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -11,8 +11,8 @@ from ..defines import ( ) from ..lv_validation import lv_bool, lv_text from ..schemas import TEXT_SCHEMA -from ..types import LvText, WidgetType -from . import Widget +from ..types import LvText +from . import Widget, WidgetType CONF_LABEL = "label" diff --git a/esphome/components/lvgl/widgets/led.py b/esphome/components/lvgl/widgets/led.py index 647973c9b7..f0092debaa 100644 --- a/esphome/components/lvgl/widgets/led.py +++ b/esphome/components/lvgl/widgets/led.py @@ -2,7 +2,7 @@ import esphome.config_validation as cv from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED from ..defines import CONF_MAIN -from ..lv_validation import lv_brightness, lv_color +from ..lv_validation import lv_color, lv_percentage from ..lvcode import lv from ..types import LvType from . import Widget, WidgetType @@ -10,7 +10,7 @@ from . import Widget, WidgetType LED_SCHEMA = cv.Schema( { cv.Optional(CONF_COLOR): lv_color, - cv.Optional(CONF_BRIGHTNESS): lv_brightness, + cv.Optional(CONF_BRIGHTNESS): lv_percentage, } ) @@ -23,7 +23,7 @@ class LedType(WidgetType): if (color := config.get(CONF_COLOR)) is not None: lv.led_set_color(w.obj, await lv_color.process(color)) if (brightness := config.get(CONF_BRIGHTNESS)) is not None: - lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) + lv.led_set_brightness(w.obj, await lv_percentage.process(brightness)) led_spec = LedType() diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index 57cb965737..a9b202163f 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -14,6 +14,7 @@ CONF_POINTS = "points" CONF_POINT_LIST_ID = "point_list_id" lv_point_t = cg.global_ns.struct("lv_point_t") +lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t") LINE_SCHEMA = { @@ -23,10 +24,7 @@ LINE_SCHEMA = { async def process_coord(coord): if isinstance(coord, Lambda): - coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) - if not coord.endswith("()"): - coord = f"static_cast({coord})" - return cg.RawExpression(coord) + return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) return cg.safe_exp(coord) diff --git a/esphome/components/lvgl/widgets/lv_bar.py b/esphome/components/lvgl/widgets/lv_bar.py index f0fdd6d278..d339807746 100644 --- a/esphome/components/lvgl/widgets/lv_bar.py +++ b/esphome/components/lvgl/widgets/lv_bar.py @@ -11,8 +11,8 @@ from ..defines import ( ) from ..lv_validation import animated, lv_int from ..lvcode import lv -from ..types import LvNumber, NumberType -from . import Widget +from ..types import LvNumber +from . import NumberType, Widget # Note this file cannot be called "bar.py" because that name is disallowed. diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index b7e3af9a78..6a7559c42c 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -1,9 +1,11 @@ from esphome import automation import esphome.codegen as cg +from esphome.components.image import get_image_metadata import esphome.config_validation as cv from esphome.const import ( CONF_COLOR, CONF_COUNT, + CONF_HEIGHT, CONF_ID, CONF_ITEMS, CONF_LENGTH, @@ -13,22 +15,39 @@ from esphome.const import ( CONF_ROTATION, CONF_VALUE, CONF_WIDTH, + CONF_X, ) +from esphome.cpp_generator import MockObj +from esphome.cpp_types import nullptr +from .. import obj_spec, set_obj_properties from ..automation import action_to_code from ..defines import ( + CHILD_ALIGNMENTS, + CONF_ALIGN, + CONF_CONTAINER, CONF_END_VALUE, CONF_INDICATOR, + CONF_LINE_WIDTH, CONF_MAIN, CONF_OPA, CONF_PIVOT_X, CONF_PIVOT_Y, + CONF_RADIUS, + CONF_SCALE, CONF_SRC, CONF_START_VALUE, CONF_TICKS, + LV_OBJ_FLAG, + LV_PART, + LV_SCALE_MODE, + get_remapped_uses, + get_warnings, ) -from ..helpers import add_lv_use, lvgl_components_required +from ..helpers import add_lv_use from ..lv_validation import ( + LV_OPA, + LV_RADIUS, get_end_value, get_start_value, lv_angle_degrees, @@ -36,105 +55,168 @@ from ..lv_validation import ( lv_color, lv_float, lv_image, + lv_int, opacity, + padding, + pixels, + pixels_or_percent, + pixels_or_percent_validator, requires_component, size, ) -from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj -from ..types import LvType, ObjUpdateAction -from . import Widget, WidgetType, get_widgets +from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj +from ..schemas import STATE_SCHEMA +from ..styles import LVStyle +from ..types import ( + LV_EVENT, + LvCompound, + LvType, + ObjUpdateAction, + lv_event_t, + lv_img_t, + lv_obj_t, +) +from . import Widget, WidgetType, get_widgets, widget_to_code from .arc import CONF_ARC from .img import CONF_IMAGE from .line import CONF_LINE -from .obj import obj_spec CONF_ANGLE_RANGE = "angle_range" CONF_COLOR_END = "color_end" CONF_COLOR_START = "color_start" +CONF_DRAW_TICKS_ON_TOP = "draw_ticks_on_top" +CONF_IMAGE_ID = "image_id" CONF_INDICATORS = "indicators" +CONF_LINE_ID = "line_id" CONF_LABEL_GAP = "label_gap" CONF_MAJOR = "major" CONF_METER = "meter" +CONF_PIVOT = "pivot" CONF_R_MOD = "r_mod" +CONF_RADIAL_OFFSET = "radial_offset" CONF_SCALES = "scales" CONF_STRIDE = "stride" CONF_TICK_STYLE = "tick_style" +# LVGL 9.4 Migration: Use scale widget instead of removed meter widget +# +# The lv_meter widget was removed in LVGL 9.4 and replaced with the more +# flexible lv_scale widget. This implementation emulates meter functionality +# using the scale widget with the following mappings: +# +# - lv_meter -> lv_scale (set to LV_SCALE_MODE_ROUND_OUTER for circular meters) +# - lv_meter_scale -> scale configuration (range, ticks, etc.) +# - lv_meter_indicator -> lv_scale_section (colored ranges on the scale) +# + + +# For compatibility, keep meter types but map to scale +lv_scale_t = LvType("lv_obj_t") lv_meter_t = LvType("lv_meter_t") -lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t") -lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr") - - -def pixels(value): - """A size in one axis in pixels""" - if isinstance(value, str) and value.lower().endswith("px"): - return cv.int_(value[:-2]) - return cv.int_(value) +lv_scale_section_t = LvType("lv_scale_section_t") +lv_meter_indicator_t = LvType("lv_meter_indicator_t") +lv_meter_indicator_ticks_t = LvType( + "lv_scale_section_t", parents=(lv_meter_indicator_t,) +) +lv_meter_indicator_arc_t = LvType("lv_scale_section_t", parents=(lv_meter_indicator_t,)) +lv_meter_indicator_line_t = LvType( + "IndicatorLine", + parents=( + LvCompound, + lv_meter_indicator_t, + ), +) +lv_meter_indicator_image_t = LvType("lv_image_t", parents=(lv_meter_indicator_t,)) +DEFAULT_LABEL_GAP = 10 # Default label gap for major ticks added by LVGL INDICATOR_LINE_SCHEMA = cv.Schema( { - cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_WIDTH, default=4): cv.int_, cv.Optional(CONF_COLOR, default=0): lv_color, - cv.Optional(CONF_R_MOD, default=0): size, - cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_R_MOD): padding, + cv.Optional(CONF_LENGTH): pixels_or_percent_validator, + cv.Optional(CONF_RADIAL_OFFSET, 0): pixels_or_percent_validator, + cv.Optional(CONF_VALUE, default=0.0): lv_float, + cv.Optional(CONF_OPA, default=1.0): opacity, } -) +).add_extra(cv.has_at_most_one_key(CONF_R_MOD, CONF_LENGTH)) + + +class ScaleType(WidgetType): + """ + Will migrate to scale.py in due course + """ + + def __init__(self): + super().__init__( + CONF_SCALE, + lv_scale_t, + (CONF_MAIN, CONF_ITEMS, CONF_INDICATOR), + {}, + is_mock=True, + ) + + +scale_spec = ScaleType() + INDICATOR_IMG_SCHEMA = cv.Schema( { cv.Required(CONF_SRC): lv_image, - cv.Required(CONF_PIVOT_X): pixels, - cv.Required(CONF_PIVOT_Y): pixels, + cv.Optional(CONF_PIVOT_X, default=0): pixels, + cv.Optional(CONF_PIVOT_Y): pixels, cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_OPA, default=1.0): opacity, } ) INDICATOR_ARC_SCHEMA = cv.Schema( { - cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_WIDTH, default=4): cv.int_, cv.Optional(CONF_COLOR, default=0): lv_color, - cv.Optional(CONF_R_MOD, default=0): size, - cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, - cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_R_MOD): padding, + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_START_VALUE): lv_float, cv.Optional(CONF_END_VALUE): lv_float, - cv.Optional(CONF_OPA): opacity, + cv.Optional(CONF_OPA, default=1.0): opacity, } -) +).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE)) + INDICATOR_TICKS_SCHEMA = cv.Schema( { - cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_WIDTH, default=4): cv.int_, cv.Optional(CONF_COLOR_START, default=0): lv_color, cv.Optional(CONF_COLOR_END): lv_color, - cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, - cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_START_VALUE): lv_float, cv.Optional(CONF_END_VALUE): lv_float, cv.Optional(CONF_LOCAL, default=False): lv_bool, } -) +).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE)) + INDICATOR_SCHEMA = cv.Schema( { cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + cv.GenerateID(): cv.declare_id(lv_meter_indicator_line_t), } ), cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All( INDICATOR_IMG_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + cv.GenerateID(): cv.declare_id(lv_meter_indicator_image_t), + cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_img_t), } ), requires_component("image"), ), cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + cv.GenerateID(): cv.declare_id(lv_meter_indicator_arc_t), } ), cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + cv.GenerateID(): cv.declare_id(lv_meter_indicator_ticks_t), } ), } @@ -142,32 +224,96 @@ INDICATOR_SCHEMA = cv.Schema( SCALE_SCHEMA = cv.Schema( { + cv.GenerateID(): cv.declare_id(lv_scale_t), cv.Optional(CONF_TICKS): cv.Schema( { cv.Optional(CONF_COUNT, default=12): cv.positive_int, - cv.Optional(CONF_WIDTH, default=2): size, + cv.Optional(CONF_WIDTH, default=2): cv.positive_int, cv.Optional(CONF_LENGTH, default=10): size, + cv.Optional(CONF_RADIAL_OFFSET, default=0): size, cv.Optional(CONF_COLOR, default=0x808080): lv_color, cv.Optional(CONF_MAJOR): cv.Schema( { cv.Optional(CONF_STRIDE, default=3): cv.positive_int, cv.Optional(CONF_WIDTH, default=5): size, cv.Optional(CONF_LENGTH, default="15%"): size, + cv.Optional(CONF_RADIAL_OFFSET, default=0): size, cv.Optional(CONF_COLOR, default=0): lv_color, cv.Optional(CONF_LABEL_GAP, default=4): size, } ), } ), - cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, - cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, - cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), + cv.Optional(CONF_RANGE_FROM, default=0.0): lv_int, + cv.Optional(CONF_RANGE_TO, default=100.0): lv_int, + cv.Optional(CONF_ANGLE_RANGE, default=270): lv_angle_degrees, cv.Optional(CONF_ROTATION): lv_angle_degrees, cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), + cv.Optional(CONF_DRAW_TICKS_ON_TOP, default=True): bool, } ) -METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)} +METER_SCHEMA = { + cv.Optional(CONF_PIVOT): STATE_SCHEMA, + cv.Optional(CONF_INDICATOR): STATE_SCHEMA, + cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA), +} + +LIGHT_STYLE = LVStyle( + "lv_meter_light", + { + "bg_opa": 1.0, + "bg_color": 0xEEEEEE, + "line_width": 1, + "line_color": 0xEEEEEE, + "arc_width": 2, + "arc_color": 0xEEEEEE, + "pad_all": 10, + "border_width": 2, + "border_color": 0xEEEEEE, + "radius": "LV_RADIUS_CIRCLE", + }, +) + +PIVOT_STYLE = { + CONF_RADIUS: LV_RADIUS.CIRCLE, + CONF_ALIGN: CHILD_ALIGNMENTS.CENTER, + "bg_color": 0x000000, + "bg_opa": 1.0, + CONF_WIDTH: 15, + CONF_HEIGHT: 15, +} + + +line_indicator_type = WidgetType( + CONF_INDICATOR, + lv_meter_indicator_line_t, + (CONF_MAIN,), + lv_name=CONF_LINE, + is_mock=True, +) + + +class SectionType(WidgetType): + def __init__(self): + super().__init__( + "scale_section", + lv_meter_indicator_arc_t, + (CONF_MAIN,), + is_mock=True, + lv_name="scale_section", + ) + + +arc_indicator_type = SectionType() + +image_indicator_type = WidgetType( + CONF_INDICATOR, + lv_meter_indicator_image_t, + (CONF_MAIN,), + lv_name=CONF_IMAGE, + is_mock=True, +) class MeterType(WidgetType): @@ -175,111 +321,241 @@ class MeterType(WidgetType): super().__init__( CONF_METER, lv_meter_t, + # Note that mapping from 8.x to 9.x, indicator styling is applied to needles, and tick styling + # is migrated to indicator (CONF_MAIN, CONF_INDICATOR, CONF_TICKS, CONF_ITEMS), METER_SCHEMA, + lv_name=CONF_CONTAINER, ) - async def to_code(self, w: Widget, config): - """For a meter object, create and set parameters""" + def get_uses(self): + return CONF_SCALE, CONF_LINE, CONF_IMAGE - lvgl_components_required.add(CONF_METER) + def validate(self, value): + return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value) + + async def on_create(self, var: MockObj, config: dict): + # Remove theme styling from outer container + lv.obj_add_style(var, await LIGHT_STYLE.get_var(), LV_PART.MAIN) + + async def create_to_code(self, config: dict, parent: MockObj): + """For a meter object using scale widget, create and set parameters""" + + add_lv_use(*self.get_uses()) + outer_config = config.copy() + indicator_config = {CONF_INDICATOR: outer_config.pop(CONF_TICKS, {})} + w = await super().create_to_code(outer_config, parent) var = w.obj + + # LVGL 9.4 scale widget setup + # Background style will be applied. for scale_conf in config.get(CONF_SCALES, ()): - rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 - if CONF_ROTATION in scale_conf: - rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION]) - with LocalVariable( - "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) - ) as meter_var: - lv.meter_set_scale_range( - var, - meter_var, - scale_conf[CONF_RANGE_FROM], - scale_conf[CONF_RANGE_TO], - scale_conf[CONF_ANGLE_RANGE], - rotation, - ) - if ticks := scale_conf.get(CONF_TICKS): - color = await lv_color.process(ticks[CONF_COLOR]) - lv.meter_set_scale_ticks( - var, - meter_var, - ticks[CONF_COUNT], - await size.process(ticks[CONF_WIDTH]), - await size.process(ticks[CONF_LENGTH]), - color, + scale_var = cg.Pvariable(scale_conf[CONF_ID], lv_expr.scale_create(var)) + percent100 = await pixels_or_percent.process(1.0) + lv_obj.set_style_height(scale_var, percent100, LV_PART.MAIN) + lv_obj.set_style_width(scale_var, percent100, LV_PART.MAIN) + lv_obj.set_style_align(scale_var, CHILD_ALIGNMENTS.CENTER, LV_PART.MAIN) + lv_obj.set_style_bg_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) + lv_obj.set_style_radius(scale_var, LV_RADIUS.CIRCLE, 0) + await set_obj_properties(Widget(scale_var, scale_spec), indicator_config) + + lv.scale_set_mode(scale_var, LV_SCALE_MODE.ROUND_INNER) + # Set the scale range + range_from = await lv_int.process(scale_conf[CONF_RANGE_FROM]) + range_to = await lv_int.process(scale_conf[CONF_RANGE_TO]) + lv.scale_set_range(scale_var, range_from, range_to) + + angle_range = await lv_angle_degrees.process(scale_conf[CONF_ANGLE_RANGE]) + if (rotation := scale_conf.get(CONF_ROTATION)) is not None: + rotation = await lv_angle_degrees.process(rotation) + else: + rotation = 90 + (360 - angle_range) // 2 + + # Set angle range + lv.scale_set_angle_range( + scale_var, + angle_range, + ) + lv.scale_set_rotation(scale_var, rotation) + + # Handle indicators as sections + for indicator in scale_conf.get(CONF_INDICATORS, ()): + (t, v) = next(iter(indicator.items())) + iid = v[CONF_ID] + + # Enable getting the meter to which this belongs. + + # Set section range based on indicator values + start_value = await get_start_value(v) or scale_conf[CONF_RANGE_FROM] + end_value = await get_end_value(v) or scale_conf[CONF_RANGE_TO] + + # Create and apply styles based on indicator type + if t == CONF_ARC: + props = { + "arc_width": v[CONF_WIDTH], + "arc_color": v[CONF_COLOR], + "arc_opa": v[CONF_OPA], + "arc_rounded": v.get("arc_rounded", False), + } + if CONF_R_MOD in v: + get_warnings().add( + "The 'r_mod' indicator property is not supported in LVGL 9.x and will be ignored." + ) + arc_style = LVStyle(f"meter_arc_{iid.id}", props) + tvar = cg.Pvariable(iid, lv_expr.scale_add_section(scale_var)) + lv.scale_section_set_style( + tvar, LV_PART.MAIN, await arc_style.get_var() ) - if CONF_MAJOR in ticks: - major = ticks[CONF_MAJOR] - lv.meter_set_scale_major_ticks( - var, - meter_var, - major[CONF_STRIDE], - await size.process(major[CONF_WIDTH]), - await size.process(major[CONF_LENGTH]), - await lv_color.process(major[CONF_COLOR]), - await size.process(major[CONF_LABEL_GAP]), - ) - for indicator in scale_conf.get(CONF_INDICATORS, ()): - (t, v) = next(iter(indicator.items())) - iid = v[CONF_ID] - ivar = cg.Pvariable(iid, cg.nullptr, type_=lv_meter_indicator_t) - # Enable getting the meter to which this belongs. - wid = Widget.create(iid, var, obj_spec, v) - wid.obj = ivar - if t == CONF_LINE: - color = await lv_color.process(v[CONF_COLOR]) - lv_assign( - ivar, - lv_expr.meter_add_needle_line( - var, - meter_var, - await size.process(v[CONF_WIDTH]), - color, - await size.process(v[CONF_R_MOD]), - ), - ) - if t == CONF_ARC: - color = await lv_color.process(v[CONF_COLOR]) - lv_assign( - ivar, - lv_expr.meter_add_arc( - var, - meter_var, - await size.process(v[CONF_WIDTH]), - color, - await size.process(v[CONF_R_MOD]), - ), - ) - if t == CONF_TICK_STYLE: - color_start = await lv_color.process(v[CONF_COLOR_START]) - color_end = await lv_color.process( - v.get(CONF_COLOR_END) or color_start - ) - lv_assign( - ivar, - lv_expr.meter_add_scale_lines( - var, - meter_var, + lw = Widget.create(iid, tvar, arc_indicator_type) + await set_indicator_values(lw, v) + + if t == CONF_TICK_STYLE: + # No object created for this + color_start = await lv_color.process(v[CONF_COLOR_START]) + color_end = await lv_color.process(v[CONF_COLOR_END]) + local = v[CONF_LOCAL] + if color_start and color_end: + async with LambdaContext( + [(lv_event_t.operator("ptr"), "e")] + ) as lambda_: + lv.scale_draw_event_cb( + lambda_.get_parameter(0), + start_value, + end_value, color_start, color_end, - v[CONF_LOCAL], - await size.process(v[CONF_WIDTH]), - ), + v[CONF_WIDTH], + local, + ) + lv_obj.add_event_cb( + scale_var, + await lambda_.get_lambda(), + LV_EVENT.DRAW_TASK_ADDED, + nullptr, ) - if t == CONF_IMAGE: - add_lv_use("img") - lv_assign( - ivar, - lv_expr.meter_add_needle_img( - var, - meter_var, - await lv_image.process(v[CONF_SRC]), - v[CONF_PIVOT_X], - v[CONF_PIVOT_Y], - ), - ) - await set_indicator_values(var, ivar, v) + lv.obj_add_flag(scale_var, LV_OBJ_FLAG.SEND_DRAW_TASK_EVENTS) + + if t == CONF_LINE: + # Needle represented by a line + if CONF_LENGTH in v: + length = v[CONF_LENGTH] + elif r_mod := v.get(CONF_R_MOD): + get_remapped_uses().add(CONF_R_MOD) + length = -abs(r_mod) + else: + length = 1.0 + props = { + CONF_ID: v[CONF_ID], + CONF_OPA: v[CONF_OPA], + CONF_LINE_WIDTH: v[CONF_WIDTH], + "line_color": v[CONF_COLOR], + "line_rounded": True, + CONF_ALIGN: CHILD_ALIGNMENTS.TOP_LEFT, + CONF_LENGTH: length, + CONF_RADIAL_OFFSET: v[CONF_RADIAL_OFFSET], + } + lw = await widget_to_code(props, line_indicator_type, scale_var) + await set_indicator_values(lw, v) + + if t == CONF_IMAGE: + add_lv_use(CONF_IMAGE) + src = v[CONF_SRC] + src_data = get_image_metadata(src.id) + pivot_x = await pixels.process(v[CONF_PIVOT_X]) + pivot_y = await pixels.process( + v.get(CONF_PIVOT_Y, src_data.height // 2) + ) + props = { + CONF_X: src_data.width // 2 - pivot_x, + "transform_pivot_x": pivot_x, + "transform_pivot_y": pivot_y, + CONF_SRC: src, + CONF_OPA: v[CONF_OPA], + CONF_ID: v[CONF_ID], + CONF_ALIGN: CHILD_ALIGNMENTS.CENTER, + } + iw = await widget_to_code(props, image_indicator_type, scale_var) + await iw.set_property(CONF_SRC, await lv_image.process(src)) + await set_indicator_values(iw, v) + + if ticks := scale_conf.get(CONF_TICKS): + # Set total tick count + lv.scale_set_total_tick_count(scale_var, ticks[CONF_COUNT]) + lv.scale_set_draw_ticks_on_top( + scale_var, scale_conf[CONF_DRAW_TICKS_ON_TOP] + ) + + # Set tick styling + lv_obj.set_style_length( + scale_var, await size.process(ticks[CONF_LENGTH]), LV_PART.ITEMS + ) + lv_obj.set_style_line_width( + scale_var, await size.process(ticks[CONF_WIDTH]), LV_PART.ITEMS + ) + lv_obj.set_style_radial_offset( + scale_var, + await size.process(ticks[CONF_RADIAL_OFFSET]), + LV_PART.ITEMS, + ) + lv_obj.set_style_line_color( + scale_var, + await lv_color.process(ticks[CONF_COLOR]), + LV_PART.ITEMS, + ) + + # Hide the scale line + lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN) + if CONF_MAJOR in ticks: + major = ticks[CONF_MAJOR] + # Set major tick frequency + lv.scale_set_major_tick_every(scale_var, major[CONF_STRIDE]) + + # Enable labels for major ticks + lv.scale_set_label_show(scale_var, True) + + # Set major tick styling + lv_obj.set_style_length( + scale_var, + await size.process(major[CONF_LENGTH]), + LV_PART.INDICATOR, + ) + lv_obj.set_style_radial_offset( + scale_var, + await size.process(ticks[CONF_RADIAL_OFFSET]), + LV_PART.INDICATOR, + ) + lv_obj.set_style_line_width( + scale_var, + await size.process(major[CONF_WIDTH]), + LV_PART.INDICATOR, + ) + lv_obj.set_style_line_color( + scale_var, + await lv_color.process(major[CONF_COLOR]), + LV_PART.INDICATOR, + ) + + # Set label gap (padding) + label_gap = await size.process(major[CONF_LABEL_GAP]) + if isinstance(label_gap, int): + label_gap -= DEFAULT_LABEL_GAP + lv_obj.set_style_pad_radial( + scale_var, + label_gap, + LV_PART.INDICATOR, + ) + else: + lv.scale_set_major_tick_every(scale_var, 0) + else: + lv.scale_set_total_tick_count(scale_var, 0) + + # Add a pivot + # Get the default style + pivot_style = PIVOT_STYLE.copy() + pivot_style.update(config.get(CONF_INDICATOR, config.get(CONF_PIVOT, {}))) + with LocalVariable("pivot", lv_obj_t, lv_expr.container_create(var)) as pivot: + pw = Widget(pivot, obj_spec, pivot_style) + await set_obj_properties(pw, pivot_style) meter_spec = MeterType() @@ -303,23 +579,39 @@ async def indicator_update_to_code(config, action_id, template_arg, args): widget = await get_widgets(config) async def set_value(w: Widget): - await set_indicator_values(w.var, w.obj, config) + await set_indicator_values(w, config) return await action_to_code( widget, set_value, action_id, template_arg, args, config ) -async def set_indicator_values(meter, indicator, config): +async def set_indicator_values(indicator: Widget, config): + """Update scale section values (replaces meter indicator values)""" start_value = await get_start_value(config) end_value = await get_end_value(config) - if start_value is not None: - if end_value is None: - lv.meter_set_indicator_value(meter, indicator, start_value) - else: - lv.meter_set_indicator_start_value(meter, indicator, start_value) - if end_value is not None: - lv.meter_set_indicator_end_value(meter, indicator, end_value) - if (opa := config.get(CONF_OPA)) is not None: - lv_assign(indicator.opa, await opacity.process(opa)) - lv_obj.invalidate(meter) + if indicator.type is arc_indicator_type: + # For scale sections, we update the range + if start_value is not None and end_value is not None: + lv.scale_section_set_range(indicator.obj, start_value, end_value) + elif start_value is not None: + # If only start value, use it as both start and end (single point) + lv.scale_section_set_range(indicator.obj, start_value, start_value) + elif end_value is not None: + # If only end value, assume range from 0 to end_value + lv.scale_section_set_range(indicator.obj, 0, end_value) + return + + if start_value is None: + return + if indicator.type is line_indicator_type: + # Line needle + lv_add(indicator.var.set_value(start_value)) + return + if indicator.type is image_indicator_type: + # Needle represented by an image + lv_obj.set_style_transform_rotation( + indicator.obj, + lv.get_needle_angle_for_value(indicator.obj, start_value) * 10, + LV_PART.MAIN, + ) diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index 82b2442378..af27ee7553 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -1,7 +1,7 @@ -from esphome import config_validation as cv -from esphome.const import CONF_BUTTON, CONF_ID, CONF_ITEMS, CONF_TEXT +from esphome import codegen as cg, config_validation as cv +from esphome.const import CONF_BUTTON, CONF_ID, CONF_TEXT from esphome.core import ID -from esphome.cpp_generator import new_Pvariable, static_const_array +from esphome.cpp_generator import MockObjClass from esphome.cpp_types import nullptr from ..defines import ( @@ -9,40 +9,82 @@ from ..defines import ( CONF_BUTTON_STYLE, CONF_BUTTONS, CONF_CLOSE_BUTTON, + CONF_HEADER_BUTTONS, + CONF_MAIN, CONF_MSGBOXES, + CONF_SRC, CONF_TITLE, + LV_OBJ_FLAG, TYPE_FLEX, + add_warning, literal, ) -from ..helpers import add_lv_use, lvgl_components_required -from ..lv_validation import lv_bool, lv_pct, lv_text -from ..lvcode import ( - EVENT_ARG, - LambdaContext, - LocalVariable, - lv, - lv_add, - lv_assign, - lv_expr, - lv_obj, - lv_Pvariable, -) -from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema, part_schema -from ..types import LV_EVENT, char_ptr, lv_obj_t -from . import Widget, add_widgets, set_obj_properties -from .button import button_spec -from .buttonmatrix import ( - BUTTONMATRIX_BUTTON_SCHEMA, - CONF_BUTTON_TEXT_LIST_ID, - buttonmatrix_spec, - get_button_data, - lv_buttonmatrix_t, - set_btn_data, +from ..helpers import add_lv_use +from ..lv_validation import lv_bool, lv_image, lv_text, pixels_or_percent +from ..lvcode import EVENT_ARG, LambdaContext, LocalVariable, lv, lv_expr, lv_obj +from ..schemas import ( + STYLE_SCHEMA, + STYLED_TEXT_SCHEMA, + TEXT_SCHEMA, + container_schema, + part_schema, ) +from ..styles import LVStyle +from ..types import LV_EVENT, lv_obj_t +from . import Widget, WidgetType, add_widgets, set_obj_properties, widget_to_code +from .button import button_spec, lv_button_t from .label import CONF_LABEL from .obj import obj_spec CONF_MSGBOX = "msgbox" + +OUTER_STYLE = LVStyle( + "msgbox_outer", + { + "bg_opa": 128, + "bg_color": "black", + "border_width": 0, + "pad_all": 0, + "radius": 0, + }, +) + + +class FooterButtonType(WidgetType): + def __init__(self): + super().__init__( + CONF_BUTTON, lv_button_t, (CONF_MAIN,), TEXT_SCHEMA, is_mock=True + ) + + async def obj_creator(self, parent: MockObjClass, config: dict): + return lv_expr.msgbox_add_footer_button(parent, config[CONF_TEXT]) + + +footer_button_spec = FooterButtonType() + + +class HeaderButtonType(WidgetType): + def __init__(self): + super().__init__( + CONF_BUTTON, + lv_button_t, + (CONF_MAIN,), + cv.Schema( + { + cv.Required(CONF_SRC): lv_image, + } + ), + is_mock=True, + ) + + async def obj_creator(self, parent: MockObjClass, config: dict): + return lv_expr.msgbox_add_header_button( + parent, await lv_image.process(config[CONF_SRC]) + ) + + +header_button_spec = HeaderButtonType() + MSGBOX_SCHEMA = container_schema( obj_spec, STYLE_SCHEMA.extend( @@ -50,10 +92,14 @@ MSGBOX_SCHEMA = container_schema( cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t), cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA, - cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA), - cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts), + cv.Optional(CONF_BUTTONS): cv.ensure_list( + container_schema(footer_button_spec) + ), + cv.Optional(CONF_HEADER_BUTTONS): cv.ensure_list( + container_schema(header_button_spec) + ), cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool, - cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), + cv.Optional(CONF_BUTTON_STYLE): part_schema(button_spec.parts), } ), ) @@ -62,7 +108,9 @@ MSGBOX_SCHEMA = container_schema( async def msgbox_to_code(top_layer, conf): """ Construct a message box. This consists of a full-screen translucent background enclosing a centered container - with an optional title, body, close button and a button matrix. And any other widgets the user cares to add + with an optional title, body, close button and a set of footer buttons. + Header buttons can be added - they can be image buttons only. + The body of the message box may have any widgets the user wants to add. :param conf: The config data :return: code to add to the init lambda """ @@ -71,60 +119,42 @@ async def msgbox_to_code(top_layer, conf): CONF_BUTTON, CONF_LABEL, CONF_MSGBOX, - *buttonmatrix_spec.get_uses(), *button_spec.get_uses(), ) - lvgl_components_required.add("BUTTONMATRIX") - messagebox_id = conf[CONF_ID] - outer_id = f"{messagebox_id.id}_outer" - outer = lv_Pvariable(lv_obj_t, messagebox_id.id + "_outer") - buttonmatrix = new_Pvariable( - ID( - f"{messagebox_id.id}_buttonmatrix_", - is_declaration=True, - type=lv_buttonmatrix_t, + if CONF_BUTTON_STYLE in conf: + add_warning( + "'button_style' for msgbox is deprecated - style the buttons directly." ) - ) - msgbox = lv_Pvariable(lv_obj_t, messagebox_id.id) - outer_widget = Widget.create(outer_id, outer, obj_spec, conf) + messagebox_id = conf[CONF_ID] + outer_id = ID(f"{messagebox_id.id}_outer", type=lv_obj_t) + outer = cg.Pvariable(outer_id, lv_expr.obj_create(top_layer)) + outer_widget = Widget.create(outer_id.id, outer, obj_spec, conf) + msgbox = cg.Pvariable(messagebox_id, lv_expr.msgbox_create(outer)) outer_widget.move_to_foreground = True msgbox_widget = Widget.create(messagebox_id, msgbox, obj_spec, conf) msgbox_widget.outer = outer_widget - buttonmatrix_widget = Widget.create( - str(buttonmatrix), buttonmatrix, buttonmatrix_spec, conf - ) - text_list, ctrl_list, width_list, _ = await get_button_data( - (conf,), buttonmatrix_widget - ) - text_id = conf[CONF_BUTTON_TEXT_LIST_ID] - text_list = static_const_array(text_id, text_list) text = await lv_text.process(conf[CONF_BODY].get(CONF_TEXT, "")) title = await lv_text.process(conf[CONF_TITLE].get(CONF_TEXT, "")) close_button = conf[CONF_CLOSE_BUTTON] - lv_assign(outer, lv_expr.obj_create(top_layer)) - lv_obj.set_width(outer, lv_pct(100)) - lv_obj.set_height(outer, lv_pct(100)) - lv_obj.set_style_bg_opa(outer, 128, 0) - lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0) - lv_obj.set_style_border_width(outer, 0, 0) - lv_obj.set_style_pad_all(outer, 0, 0) - lv_obj.set_style_radius(outer, 0, 0) - outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") - lv_assign( - msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button) - ) + percent100 = await pixels_or_percent.process(1.0) + lv_obj.set_size(outer, percent100, percent100) + outer_widget.add_style(await OUTER_STYLE.get_var()) + outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN) + lv.msgbox_add_title(msgbox, title) + lv.msgbox_add_text(msgbox, text) lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0) - lv_add(buttonmatrix.set_obj(lv_expr.msgbox_get_btns(msgbox))) - if button_style := conf.get(CONF_BUTTON_STYLE): - button_style = {CONF_ITEMS: button_style} - await set_obj_properties(buttonmatrix_widget, button_style) await set_obj_properties(msgbox_widget, conf) await add_widgets(msgbox_widget, conf) + for button in conf.get(CONF_BUTTONS, ()): + await widget_to_code(button, footer_button_spec, msgbox) + for button in conf.get(CONF_HEADER_BUTTONS, ()): + await widget_to_code(button, header_button_spec, msgbox) + async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action: - outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") + outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN) if close_button: with LocalVariable( - "close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox) + "close_btn_", lv_obj_t, lv_expr.msgbox_add_close_button(msgbox) ) as close_btn: lv_obj.remove_event_cb(close_btn, nullptr) lv_obj.add_event_cb( @@ -138,9 +168,6 @@ async def msgbox_to_code(top_layer, conf): outer, await close_action.get_lambda(), LV_EVENT.CLICKED, nullptr ) - if len(ctrl_list) != 0 or len(width_list) != 0: - set_btn_data(buttonmatrix.obj, ctrl_list, width_list) - async def msgboxes_to_code(lv_component, config): top_layer = lv.disp_get_layer_top(lv_component.get_disp()) diff --git a/esphome/components/lvgl/widgets/obj.py b/esphome/components/lvgl/widgets/obj.py index ab22a5ce86..079a0734ef 100644 --- a/esphome/components/lvgl/widgets/obj.py +++ b/esphome/components/lvgl/widgets/obj.py @@ -1,5 +1,6 @@ from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR -from ..types import WidgetType, lv_obj_t +from ..types import lv_obj_t +from . import WidgetType class ObjType(WidgetType): diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index ad46f67c6b..82c4370543 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -1,14 +1,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT -from esphome.cpp_generator import MockObjClass -from ..defines import CONF_MAIN -from ..lv_validation import lv_color, lv_text -from ..lvcode import LocalVariable, lv, lv_expr +from ..defines import CONF_MAIN, get_color_formats +from ..lv_validation import color, lv_color, lv_int, lv_text +from ..lvcode import LocalVariable, lv from ..schemas import TEXT_SCHEMA -from ..types import WidgetType, lv_obj_t -from . import Widget +from ..types import lv_obj_t +from . import Widget, WidgetType +from .canvas import CONF_CANVAS +from .img import CONF_IMAGE CONF_QRCODE = "qrcode" CONF_DARK_COLOR = "dark_color" @@ -16,9 +17,16 @@ CONF_LIGHT_COLOR = "light_color" QRCODE_SCHEMA = { **TEXT_SCHEMA, - cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, - cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, - cv.Required(CONF_SIZE): cv.int_, + cv.Optional(CONF_DARK_COLOR, default="black"): color, + cv.Optional(CONF_LIGHT_COLOR, default="white"): color, + cv.Required(CONF_SIZE): lv_int, +} + +QRCODE_MODIFY_SCHEMA = { + **TEXT_SCHEMA, + cv.Optional(CONF_DARK_COLOR): lv_color, + cv.Optional(CONF_LIGHT_COLOR): lv_color, + cv.Optional(CONF_SIZE): lv_int, } @@ -29,20 +37,25 @@ class QrCodeType(WidgetType): lv_obj_t, (CONF_MAIN,), QRCODE_SCHEMA, - modify_schema=TEXT_SCHEMA, + modify_schema=QRCODE_MODIFY_SCHEMA, ) def get_uses(self): - return "canvas", "img", "label" - - async def obj_creator(self, parent: MockObjClass, config: dict): - dark_color = await lv_color.process(config[CONF_DARK_COLOR]) - light_color = await lv_color.process(config[CONF_LIGHT_COLOR]) - size = config[CONF_SIZE] - return lv_expr.call("qrcode_create", parent, size, dark_color, light_color) + return CONF_CANVAS, CONF_IMAGE async def to_code(self, w: Widget, config): + get_color_formats().add("ARGB8888") + await w.set_property( + CONF_LIGHT_COLOR, await lv_color.process(config.get(CONF_LIGHT_COLOR)) + ) + await w.set_property( + CONF_DARK_COLOR, await lv_color.process(config.get(CONF_DARK_COLOR)) + ) + await w.set_property(CONF_SIZE, await lv_int.process(config.get(CONF_SIZE))) if (value := config.get(CONF_TEXT)) is not None: + if isinstance(value, str): + lv.qrcode_update(w.obj, value, len(value)) + return value = await lv_text.process(value) with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj: lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size()) diff --git a/esphome/components/lvgl/widgets/slider.py b/esphome/components/lvgl/widgets/slider.py index d5017668e4..85096c302a 100644 --- a/esphome/components/lvgl/widgets/slider.py +++ b/esphome/components/lvgl/widgets/slider.py @@ -2,18 +2,18 @@ import esphome.config_validation as cv from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE from ..defines import ( - BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_KNOB, CONF_MAIN, + SLIDER_MODES, literal, ) -from ..helpers import add_lv_use from ..lv_validation import animated, get_start_value, lv_float from ..lvcode import lv -from ..types import LvNumber, NumberType -from . import Widget +from ..types import LvNumber +from . import NumberType, Widget +from .label import CONF_LABEL from .lv_bar import CONF_BAR CONF_SLIDER = "slider" @@ -29,7 +29,7 @@ SLIDER_SCHEMA = cv.Schema( cv.Optional(CONF_VALUE): lv_float, cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, - cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_MODE, default="NORMAL"): SLIDER_MODES.one_of, cv.Optional(CONF_ANIMATED, default=True): animated, } ) @@ -49,8 +49,10 @@ class SliderType(NumberType): def animated(self): return True + def get_uses(self): + return (CONF_BAR, CONF_LABEL) + async def to_code(self, w: Widget, config): - add_lv_use(CONF_BAR) if CONF_MIN_VALUE in config: # not modify case lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) diff --git a/esphome/components/lvgl/widgets/spinner.py b/esphome/components/lvgl/widgets/spinner.py index 83aac25a59..0e409ba3b7 100644 --- a/esphome/components/lvgl/widgets/spinner.py +++ b/esphome/components/lvgl/widgets/spinner.py @@ -1,9 +1,8 @@ import esphome.config_validation as cv -from esphome.cpp_generator import MockObjClass from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME from ..lv_validation import lv_angle_degrees, lv_milliseconds -from ..lvcode import lv_expr +from ..lvcode import lv from ..types import LvType from . import Widget, WidgetType from .arc import CONF_ARC @@ -12,8 +11,10 @@ CONF_SPINNER = "spinner" SPINNER_SCHEMA = cv.Schema( { - cv.Required(CONF_ARC_LENGTH): lv_angle_degrees, - cv.Required(CONF_SPIN_TIME): lv_milliseconds, + cv.Optional(CONF_ARC_LENGTH, default=200): cv.All( + lv_angle_degrees, cv.int_range(min=0, max=360) + ), + cv.Optional(CONF_SPIN_TIME, default="2s"): lv_milliseconds, } ) @@ -25,19 +26,17 @@ class SpinnerType(WidgetType): LvType("lv_spinner_t"), (CONF_MAIN, CONF_INDICATOR), SPINNER_SCHEMA, - {}, ) async def to_code(self, w: Widget, config): - return [] + spin_time = await lv_milliseconds.process(config.get(CONF_SPIN_TIME)) + arc_length = int(config[CONF_ARC_LENGTH]) + if arc_length < 180: + arc_length += 180 + lv.spinner_set_anim_params(w.obj, spin_time, arc_length) def get_uses(self): return (CONF_ARC,) - async def obj_creator(self, parent: MockObjClass, config: dict): - spin_time = await lv_milliseconds.process(config[CONF_SPIN_TIME]) - arc_length = await lv_angle_degrees.process(config[CONF_ARC_LENGTH]) - return lv_expr.call("spinner_create", parent, spin_time, arc_length) - spinner_spec = SpinnerType() diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index cd7cf7b471..60ba664f04 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -1,8 +1,14 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE -from esphome.cpp_generator import MockObjClass +from esphome.const import ( + CONF_ID, + CONF_INDEX, + CONF_ITEMS, + CONF_NAME, + CONF_POSITION, + CONF_SIZE, +) from ..automation import action_to_code from ..defines import ( @@ -15,10 +21,11 @@ from ..defines import ( literal, ) from ..lv_validation import animated, lv_int, size -from ..lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj from ..schemas import container_schema, part_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from .button import button_spec from .buttonmatrix import buttonmatrix_spec from .obj import obj_spec @@ -69,6 +76,10 @@ class TabviewType(WidgetType): return "btnmatrix", TYPE_FLEX async def to_code(self, w: Widget, config: dict): + await w.set_property( + "tab_bar_position", await DIRECTIONS.process(config[CONF_POSITION]) + ) + await w.set_property("tab_bar_size", await size.process(config[CONF_SIZE])) for tab_conf in config[CONF_TABS]: w_id = tab_conf[CONF_ID] tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t) @@ -76,25 +87,27 @@ class TabviewType(WidgetType): lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME])) await set_obj_properties(tab_widget, tab_conf) await add_widgets(tab_widget, tab_conf) - if button_style := config.get(CONF_TAB_STYLE): + tab_style = config.get(CONF_TAB_STYLE, {}) + tab_items_style = tab_style.get(CONF_ITEMS, {}) + if tab_style: with LocalVariable( - "tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj) - ) as btnmatrix_obj: - await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style) + "tabview_bar", lv_obj_t, rhs=lv_expr.tabview_get_tab_bar(w.obj) + ) as bar_obj: + tab_bar = Widget(bar_obj, obj_spec) + await set_obj_properties(tab_bar, tab_style) + if tab_items_style: + for index, tab_conf in enumerate(config[CONF_TABS]): + await set_obj_properties( + Widget(lv_obj.get_child(bar_obj, index), button_spec), + tab_items_style, + ) + if content_style := config.get(CONF_CONTENT_STYLE): with LocalVariable( "tabview_content", lv_obj_t, rhs=lv_expr.tabview_get_content(w.obj) ) as content_obj: await set_obj_properties(Widget(content_obj, obj_spec), content_style) - async def obj_creator(self, parent: MockObjClass, config: dict): - return lv_expr.call( - "tabview_create", - parent, - await DIRECTIONS.process(config[CONF_POSITION]), - await size.process(config[CONF_SIZE]), - ) - tabview_spec = TabviewType() @@ -117,6 +130,6 @@ async def tabview_select(config, action_id, template_arg, args): async def do_select(w: Widget): lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED])) - lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + lv.obj_send_event(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) return await action_to_code(widget, do_select, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index 430a386d2e..dadaef7d07 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -15,7 +15,7 @@ from ..defines import ( TILE_DIRECTIONS, literal, ) -from ..lv_validation import animated, lv_int, lv_pct +from ..lv_validation import animated, lv_int, pixels_or_percent from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable from ..schemas import container_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr @@ -68,17 +68,19 @@ class TileviewType(WidgetType): w_id = tile_conf[CONF_ID] tile_obj = lv_Pvariable(lv_obj_t, w_id) tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) - dirs = tile_conf[CONF_DIR] - if isinstance(dirs, list): - dirs = "|".join(dirs) + dirs = await TILE_DIRECTIONS.process(tile_conf[CONF_DIR]) row_pos = tile_conf[CONF_ROW] col_pos = tile_conf[CONF_COLUMN] lv_assign( tile_obj, - lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, literal(dirs)), + lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, dirs), ) # Bugfix for LVGL 8.x - lv_obj.set_pos(tile_obj, lv_pct(col_pos * 100), lv_pct(row_pos * 100)) + lv_obj.set_pos( + tile_obj, + await pixels_or_percent.process(float(col_pos)), + await pixels_or_percent.process(float(row_pos)), + ) await set_obj_properties(tile, tile_conf) await add_widgets(tile, tile_conf) if tiles: diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 99129880f4..8370977ce2 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -15,8 +15,7 @@ void MAX31855Sensor::update() { this->disable(); // Conversion time typ: 170ms, max: 220ms - auto f = std::bind(&MAX31855Sensor::read_data_, this); - this->set_timeout("value", 220, f); + this->set_timeout("value", 220, [this]() { this->read_data_(); }); } void MAX31855Sensor::setup() { this->spi_setup(); } diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index ff65c8c5c9..35e12309ba 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -41,8 +41,8 @@ void MAX31856Sensor::update() { this->one_shot_temperature_(); // Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz - auto f = std::bind(&MAX31856Sensor::read_thermocouple_temperature_, this); - this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, f); + this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, + [this]() { this->read_thermocouple_temperature_(); }); } void MAX31856Sensor::read_thermocouple_temperature_() { diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 09b8368d07..8b06a01166 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -60,8 +60,7 @@ void MAX31865Sensor::update() { this->write_config_(0b11100000, 0b10100000); // Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz - auto f = std::bind(&MAX31865Sensor::read_data_, this); - this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, f); + this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, [this]() { this->read_data_(); }); } void MAX31865Sensor::setup() { diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index c329cdfd42..b8527c6b1d 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -13,8 +13,7 @@ void MAX6675Sensor::update() { this->disable(); // Conversion time typ: 170ms, max: 220ms - auto f = std::bind(&MAX6675Sensor::read_data_, this); - this->set_timeout("value", 250, f); + this->set_timeout("value", 250, [this]() { this->read_data_(); }); } void MAX6675Sensor::setup() { this->spi_setup(); } diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 90e7bf75b5..031f6657f4 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -80,12 +80,15 @@ class StateTrigger : public Trigger<> { template class MediaPlayerStateTrigger : public Trigger<> { public: - explicit MediaPlayerStateTrigger(MediaPlayer *player) { - player->add_on_state_callback([this, player]() { - if (player->state == State) + explicit MediaPlayerStateTrigger(MediaPlayer *player) : player_(player) { + player->add_on_state_callback([this]() { + if (this->player_->state == State) this->trigger(); }); } + + protected: + MediaPlayer *player_; }; using IdleTrigger = MediaPlayerStateTrigger; diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index bccea7d423..ff518808d9 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -16,6 +16,7 @@ static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM[] = {0xFF, 0x01, 0x static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88}; static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x27, 0x10}; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG static const LogString *detection_range_to_log_string(MHZ19DetectionRange range) { switch (range) { case MHZ19_DETECTION_RANGE_0_2000PPM: @@ -28,6 +29,7 @@ static const LogString *detection_range_to_log_string(MHZ19DetectionRange range) return LOG_STR("default"); } } +#endif uint8_t mhz19_checksum(const uint8_t *command) { uint8_t sum = 0; diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 660d185b49..c7737ba7d6 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -59,7 +59,7 @@ template class ApplianceBase : public Component { public: ApplianceBase() { this->base_.setStream(&this->stream_); - this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); + this->base_.addOnStateCallback([this]() { this->on_status_change(); }); dudanov::midea::ApplianceBase::setLogger( [](int level, const char *tag, int line, const String &format, va_list args) { esp_log_vprintf_(level, tag, line, format.c_str(), args); diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 7a61868e6e..4146a54c87 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -415,10 +415,10 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) { size_t at = this->rx_buffer_.size(); if (at > 0) { if (warn) { - ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), millis() - this->last_send_); } else { - ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), + ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason), millis() - this->last_send_); } this->rx_buffer_.clear(); diff --git a/esphome/components/mqtt/custom_mqtt_device.h b/esphome/components/mqtt/custom_mqtt_device.h index 09ed7bd6d1..a003379fe0 100644 --- a/esphome/components/mqtt/custom_mqtt_device.h +++ b/esphome/components/mqtt/custom_mqtt_device.h @@ -189,28 +189,33 @@ class CustomMQTTDevice { template void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(const std::string &, const std::string &), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &t, const std::string &payload) { (obj->*callback)(t, payload); }, qos); } template void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(const std::string &), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_2); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &, const std::string &payload) { (obj->*callback)(payload); }, qos); } template void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(), uint8_t qos) { - auto f = std::bind(callback, (T *) this); - global_mqtt_client->subscribe(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe( + topic, [obj, callback](const std::string &, const std::string &) { (obj->*callback)(); }, qos); } template void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); - global_mqtt_client->subscribe_json(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe_json( + topic, [obj, callback](const std::string &t, JsonObject root) { (obj->*callback)(t, root); }, qos); } template void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject), uint8_t qos) { - auto f = std::bind(callback, (T *) this, std::placeholders::_2); - global_mqtt_client->subscribe_json(topic, f, qos); + auto *obj = static_cast(this); + global_mqtt_client->subscribe_json( + topic, [obj, callback](const std::string &, JsonObject root) { (obj->*callback)(root); }, qos); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index a461a140ae..74a60b3624 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -23,27 +23,52 @@ static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) { MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + +static bool apply_command(AlarmControlPanelCall &call, const char *state) { + if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_AWAY")) == 0) { + call.arm_away(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_HOME")) == 0) { + call.arm_home(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_NIGHT")) == 0) { + call.arm_night(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_VACATION")) == 0) { + call.arm_vacation(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_CUSTOM_BYPASS")) == 0) { + call.arm_custom_bypass(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("DISARM")) == 0) { + call.disarm(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("PENDING")) == 0) { + call.pending(); + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("TRIGGERED")) == 0) { + call.triggered(); + } else { + return false; + } + return true; +} + void MQTTAlarmControlPanelComponent::setup() { this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto call = this->alarm_control_panel_->make_call(); - if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_AWAY")) == 0) { - call.arm_away(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_HOME")) == 0) { - call.arm_home(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_NIGHT")) == 0) { - call.arm_night(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_VACATION")) == 0) { - call.arm_vacation(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_CUSTOM_BYPASS")) == 0) { - call.arm_custom_bypass(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("DISARM")) == 0) { - call.disarm(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("PENDING")) == 0) { - call.pending(); - } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("TRIGGERED")) == 0) { - call.triggered(); - } else { + if (!payload.empty() && payload[0] == '{') { + // JSON payload: {"state": "DISARM", "code": "1234"} + JsonDocument doc = json::parse_json(payload); + JsonObject root = doc.as(); + if (!root.isNull()) { + const char *state = root[ESPHOME_F("state")]; + if (state == nullptr) { + ESP_LOGW(TAG, "'%s': JSON payload missing 'state' key", this->friendly_name_().c_str()); + } else if (!apply_command(call, state)) { + ESP_LOGW(TAG, "'%s': Received unknown state in JSON payload: %s", this->friendly_name_().c_str(), state); + } else { + const char *code = root[ESPHOME_F("code")]; + if (code != nullptr) { + call.set_code(code); + } + } + } + } else if (!apply_command(call, payload.c_str())) { ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); } call.perform(); diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 38daf8f8f6..ab665e2579 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -28,6 +28,10 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt"; +// Maximum number of MQTT component resends per loop iteration. +// Limits work to avoid triggering the task watchdog on reconnect. +static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8; + // 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", @@ -396,9 +400,16 @@ void MQTTClientComponent::loop() { this->resubscribe_subscriptions_(); // Process pending resends for all MQTT components centrally - // This is more efficient than each component polling in its own loop - for (MQTTComponent *component : this->children_) { - component->process_resend(); + // Limit work per loop iteration to avoid triggering task WDT on reconnect + { + uint8_t resend_count = 0; + for (MQTTComponent *component : this->children_) { + if (component->is_resend_pending()) { + component->process_resend(); + if (++resend_count >= MAX_RESENDS_PER_LOOP) + break; + } + } } } break; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 127e4073b0..14473f737a 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -366,14 +366,14 @@ class MQTTJsonMessageTrigger : public Trigger { class MQTTConnectTrigger : public Trigger { public: - explicit MQTTConnectTrigger(MQTTClientComponent *&client) { + explicit MQTTConnectTrigger(MQTTClientComponent *client) { client->set_on_connect([this](bool session_present) { this->trigger(session_present); }); } }; class MQTTDisconnectTrigger : public Trigger { public: - explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) { + explicit MQTTDisconnectTrigger(MQTTClientComponent *client) { client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); }); } }; @@ -405,15 +405,14 @@ template class MQTTPublishJsonAction : public Action { void set_payload(std::function payload) { this->payload_ = payload; } void play(const Ts &...x) override { - auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); auto qos = this->qos_.value(x...); auto retain = this->retain_.value(x...); - this->parent_->publish_json(topic, f, qos, retain); + this->parent_->publish_json( + topic, [this, x...](JsonObject root) { this->payload_(x..., root); }, qos, retain); } protected: - void encode_(Ts... x, JsonObject root) { this->payload_(x..., root); } std::function payload_; MQTTClientComponent *parent_; }; diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2403ef64ea..7983e04870 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -147,6 +147,9 @@ class MQTTComponent : public Component { /// Internal method for the MQTT client base to schedule a resend of the state on reconnect. void schedule_resend_state(); + /// Check if a resend is pending (called by MQTTClientComponent to rate-limit work) + bool is_resend_pending() const { return this->resend_state_; } + /// Process pending resend if needed (called by MQTTClientComponent) void process_resend(); diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index ae2b8c4600..3a8658a55f 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -121,8 +121,7 @@ void MQTTFanComponent::setup() { }); } - auto f = std::bind(&MQTTFanComponent::publish_state, this); - this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); + this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); } void MQTTFanComponent::dump_config() { diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 7ed73400c8..d47ca245b8 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -45,8 +45,7 @@ void MS5611Component::update() { return; } - auto f = std::bind(&MS5611Component::read_temperature_, this); - this->set_timeout("temperature", 10, f); + this->set_timeout("temperature", 10, [this]() { this->read_temperature_(); }); } void MS5611Component::read_temperature_() { uint8_t bytes[3]; @@ -62,8 +61,7 @@ void MS5611Component::read_temperature_() { return; } - auto f = std::bind(&MS5611Component::read_pressure_, this, raw_temperature); - this->set_timeout("pressure", 10, f); + this->set_timeout("pressure", 10, [this, raw_temperature]() { this->read_pressure_(raw_temperature); }); } void MS5611Component::read_pressure_(uint32_t raw_temperature) { uint8_t bytes[3]; diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index 88a3e6d7dc..d141dcb191 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -281,9 +281,8 @@ void MS8607Component::request_read_temperature_() { return; } - auto f = std::bind(&MS8607Component::read_temperature_, this); // datasheet says 17.2ms max conversion time at OSR 8192 - this->set_timeout("temperature", 20, f); + this->set_timeout("temperature", 20, [this]() { this->read_temperature_(); }); } void MS8607Component::read_temperature_() { @@ -303,9 +302,8 @@ void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) { return; } - auto f = std::bind(&MS8607Component::read_pressure_, this, d2_raw_temperature); // datasheet says 17.2ms max conversion time at OSR 8192 - this->set_timeout("pressure", 20, f); + this->set_timeout("pressure", 20, [this, d2_raw_temperature]() { this->read_pressure_(d2_raw_temperature); }); } void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) { @@ -325,9 +323,8 @@ void MS8607Component::request_read_humidity_(float temperature_float) { return; } - auto f = std::bind(&MS8607Component::read_humidity_, this, temperature_float); // datasheet says 15.89ms max conversion time at OSR 8192 - this->set_timeout("humidity", 20, f); + this->set_timeout("humidity", 20, [this, temperature_float]() { this->read_humidity_(temperature_float); }); } void MS8607Component::read_humidity_(float temperature_float) { diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 937239b98d..66d36dd741 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -83,8 +83,7 @@ void NAU7802Sensor::setup() { // turn on AFE pu_ctrl |= PU_CTRL_POWERUP_ANALOG; - auto f = std::bind(&NAU7802Sensor::complete_setup_, this); - this->set_timeout(600, f); + this->set_timeout(600, [this]() { this->complete_setup_(); }); } void NAU7802Sensor::complete_setup_() { diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 226b11b8cd..79ddd3844c 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -42,29 +42,5 @@ network::IPAddresses get_ip_addresses() { return {}; } -const char *get_use_address() { - // Global component pointers are guaranteed to be set by component constructors when USE_* is defined -#ifdef USE_ETHERNET - return ethernet::global_eth_component->get_use_address(); -#endif - -#ifdef USE_MODEM - return modem::global_modem_component->get_use_address(); -#endif - -#ifdef USE_WIFI - return wifi::global_wifi_component->get_use_address(); -#endif - -#ifdef USE_OPENTHREAD - return openthread::global_openthread_component->get_use_address(); -#endif - -#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) - // Fallback when no network component is defined (e.g., host platform) - return ""; -#endif -} - } // namespace esphome::network #endif diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 4b700fe74c..e4e8a01f8c 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -54,7 +54,29 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() { /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname -const char *get_use_address(); +ESPHOME_ALWAYS_INLINE inline const char *get_use_address() { + // Global component pointers are guaranteed to be set by component constructors when USE_* is defined +#ifdef USE_ETHERNET + return ethernet::global_eth_component->get_use_address(); +#endif + +#ifdef USE_MODEM + return modem::global_modem_component->get_use_address(); +#endif + +#ifdef USE_WIFI + return wifi::global_wifi_component->get_use_address(); +#endif + +#ifdef USE_OPENTHREAD + return openthread::global_openthread_component->get_use_address(); +#endif + +#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) + // Fallback when no network component is defined (e.g., host platform) + return ""; +#endif +} IPAddresses get_ip_addresses(); } // namespace esphome::network diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 27a857c112..aac9b2a23d 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -80,35 +80,25 @@ void NumberCall::perform() { target_value = max_value; } } else if (this->operation_ == NUMBER_OP_INCREMENT) { - ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't increment, no state")); return; } auto step = traits.get_step(); target_value = parent->state + (std::isnan(step) ? 1 : step); - if (target_value > max_value) { - if (this->cycle_ && !std::isnan(min_value)) { - target_value = min_value; - } else { - target_value = max_value; - } - } + if (target_value > max_value) + target_value = this->cycle_or_clamp_(max_value, min_value); } else if (this->operation_ == NUMBER_OP_DECREMENT) { - ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out"); + ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); if (!parent->has_state()) { this->log_perform_warning_(LOG_STR("Can't decrement, no state")); return; } auto step = traits.get_step(); target_value = parent->state - (std::isnan(step) ? 1 : step); - if (target_value < min_value) { - if (this->cycle_ && !std::isnan(max_value)) { - target_value = max_value; - } else { - target_value = min_value; - } - } + if (target_value < min_value) + target_value = this->cycle_or_clamp_(min_value, max_value); } if (target_value < min_value) { diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 584c13f413..29eaeb72d9 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -33,6 +33,9 @@ class NumberCall { NumberCall &with_cycle(bool cycle); protected: + float cycle_or_clamp_(float clamp, float opposite) const { + return (this->cycle_ && !std::isnan(opposite)) ? opposite : clamp; + } void log_perform_warning_(const LogString *message); void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val, float limit); diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 35a9de3537..292e2bb3bb 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -5,12 +5,14 @@ import esphome.codegen as cg from esphome.components import runtime_image from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent +from esphome.components.image import CONF_TRANSPARENCY, add_metadata import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TRIGGER_ID, + CONF_TYPE, CONF_URL, ) from esphome.core import Lambda @@ -131,6 +133,13 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): # Use the enhanced helper function to get all runtime image parameters settings = await runtime_image.process_runtime_image_config(config) + add_metadata( + config[CONF_ID], + settings.width, + settings.height, + config[CONF_TYPE], + config[CONF_TRANSPARENCY], + ) url = config[CONF_URL] var = cg.new_Pvariable( diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 1596b6e990..7c9a308303 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -257,11 +257,5 @@ void OpenThreadComponent::on_factory_reset(std::function callback) { ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); } -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *OpenThreadComponent::get_use_address() const { return this->use_address_; } - -void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } - } // namespace esphome::openthread #endif diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index bd10774fcf..b42fdd2d30 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -37,8 +37,8 @@ class OpenThreadComponent : public Component { void on_factory_reset(std::function callback); void defer_factory_reset_external_callback(); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } #if CONFIG_OPENTHREAD_MTD void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 1930e45e85..0b166bb65c 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -177,13 +177,19 @@ async def register_packet_transport(var, config): cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) is_provider = False - for sens_conf in config.get(CONF_SENSORS, ()): + sensors = config.get(CONF_SENSORS, ()) + binary_sensors = config.get(CONF_BINARY_SENSORS, ()) + if sensors: + cg.add(var.set_sensor_count(len(sensors))) + if binary_sensors: + cg.add(var.set_binary_sensor_count(len(binary_sensors))) + for sens_conf in sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) cg.add(var.add_sensor(bcst_id, sensor)) - for sens_conf in config.get(CONF_BINARY_SENSORS, ()): + for sens_conf in binary_sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 964037a02c..a2199977aa 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -221,16 +221,20 @@ void PacketTransport::setup() { } #ifdef USE_SENSOR for (auto &sensor : this->sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](float x) { - this->updated_ = true; + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + sensor.sensor->add_on_state_callback([&sensor](float x) { + sensor.parent->updated_ = true; sensor.updated = true; }); } #endif #ifdef USE_BINARY_SENSOR for (auto &sensor : this->binary_sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](bool value) { - this->updated_ = true; + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + sensor.sensor->add_on_state_callback([&sensor](bool value) { + sensor.parent->updated_ = true; sensor.updated = true; }); } @@ -548,11 +552,11 @@ void PacketTransport::dump_config() { " Ping-pong: %s", this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_)); #ifdef USE_SENSOR - for (auto sensor : this->sensors_) + for (const auto &sensor : this->sensors_) ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); #endif #ifdef USE_BINARY_SENSOR - for (auto sensor : this->binary_sensors_) + for (const auto &sensor : this->binary_sensors_) ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); #endif for (const auto &host : this->providers_) { diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index b3798738e2..836775bc85 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" @@ -37,11 +38,14 @@ struct Provider { #endif }; +class PacketTransport; + #ifdef USE_SENSOR struct Sensor { sensor::Sensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif #ifdef USE_BINARY_SENSOR @@ -49,6 +53,7 @@ struct BinarySensor { binary_sensor::BinarySensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif @@ -60,8 +65,9 @@ class PacketTransport : public PollingComponent { void dump_config() override; #ifdef USE_SENSOR + void set_sensor_count(size_t count) { this->sensors_.init(count); } void add_sensor(const char *id, sensor::Sensor *sensor) { - Sensor st{sensor, id, true}; + Sensor st{sensor, id, true, this}; this->sensors_.push_back(st); } void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { @@ -70,8 +76,9 @@ class PacketTransport : public PollingComponent { } #endif #ifdef USE_BINARY_SENSOR + void set_binary_sensor_count(size_t count) { this->binary_sensors_.init(count); } void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { - BinarySensor st{sensor, id, true}; + BinarySensor st{sensor, id, true, this}; this->binary_sensors_.push_back(st); } @@ -141,11 +148,11 @@ class PacketTransport : public PollingComponent { std::vector encryption_key_{}; #ifdef USE_SENSOR - std::vector sensors_{}; + FixedVector sensors_{}; string_map_t> remote_sensors_{}; #endif #ifdef USE_BINARY_SENSOR - std::vector binary_sensors_{}; + FixedVector binary_sensors_{}; string_map_t> remote_binary_sensors_{}; #endif diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 114ecf435e..6275ff60c2 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -95,10 +95,6 @@ void PMSX003Component::loop() { // Just go ahead and read stuff break; } - } else if (now - this->last_update_ < this->update_interval_) { - // Otherwise just leave the sensor powered up and come back when we hit the update - // time - return; } if (now - this->last_transmission_ >= 500) { @@ -114,10 +110,11 @@ void PMSX003Component::loop() { this->read_byte(&this->data_[this->data_index_]); auto check = this->check_byte_(); if (!check.has_value()) { - // finished - this->parse_data_(); + if (this->update_interval_ > STABILISING_MS || now - this->last_update_ >= this->update_interval_) { + this->parse_data_(); + this->last_update_ = now; + } this->data_index_ = 0; - this->last_update_ = now; } else if (!*check) { // wrong data this->data_index_ = 0; @@ -138,7 +135,7 @@ optional PMSX003Component::check_byte_() { return true; } - ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, START_CHARACTER_1); + ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, start_char); return false; } diff --git a/esphome/components/preferences/__init__.py b/esphome/components/preferences/__init__.py index c6bede891a..c426872728 100644 --- a/esphome/components/preferences/__init__.py +++ b/esphome/components/preferences/__init__.py @@ -21,5 +21,9 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.PREFERENCES) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - cg.add(var.set_write_interval(config[CONF_FLASH_WRITE_INTERVAL])) + write_interval = config[CONF_FLASH_WRITE_INTERVAL] + if write_interval.total_milliseconds == 0: + cg.add_define("USE_PREFERENCES_SYNC_EVERY_LOOP") + else: + cg.add(var.set_write_interval(write_interval)) await cg.register_component(var, config) diff --git a/esphome/components/preferences/syncer.h b/esphome/components/preferences/syncer.h index 96716d3f30..e28cc8c8d5 100644 --- a/esphome/components/preferences/syncer.h +++ b/esphome/components/preferences/syncer.h @@ -8,24 +8,21 @@ namespace preferences { class IntervalSyncer final : public Component { public: +#ifdef USE_PREFERENCES_SYNC_EVERY_LOOP + void loop() override { global_preferences->sync(); } +#else void set_write_interval(uint32_t write_interval) { this->write_interval_ = write_interval; } void setup() override { - if (this->write_interval_ != 0) { - set_interval(this->write_interval_, []() { global_preferences->sync(); }); - // When using interval-based syncing, we don't need the loop - this->disable_loop(); - } - } - void loop() override { - if (this->write_interval_ == 0) { - global_preferences->sync(); - } + this->set_interval(this->write_interval_, []() { global_preferences->sync(); }); } +#endif void on_shutdown() override { global_preferences->sync(); } float get_setup_priority() const override { return setup_priority::BUS; } +#ifndef USE_PREFERENCES_SYNC_EVERY_LOOP protected: uint32_t write_interval_{60000}; +#endif }; } // namespace preferences diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index bf17ac27b8..a0594d7f67 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -310,6 +310,50 @@ async def beo4_action(var, config, args): cg.add(var.set_repeats(template_)) +# Brennenstuhl +( + BrennenstuhlData, + BrennenstuhlBinarySensor, + BrennenstuhlTrigger, + BrennenstuhlAction, + BrennenstuhlDumper, +) = declare_protocol("Brennenstuhl") + +BRENNENSTUHL_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.hex_uint32_t, + } +) + + +@register_binary_sensor("brennenstuhl", BrennenstuhlBinarySensor, BRENNENSTUHL_SCHEMA) +def brennenstuhl_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + BrennenstuhlData, + ("code", config[CONF_CODE]), + ) + ) + ) + + +@register_trigger("brennenstuhl", BrennenstuhlTrigger, BrennenstuhlData) +def brennenstuhl_trigger(var, config): + pass + + +@register_dumper("brennenstuhl", BrennenstuhlDumper) +def brennenstuhl_dumper(var, config): + pass + + +@register_action("brennenstuhl", BrennenstuhlAction, BRENNENSTUHL_SCHEMA) +async def brennenstuhl_action(var, config, args): + template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32) + cg.add(var.set_code(template_)) + + # ByronSX ( ByronSXData, diff --git a/esphome/components/remote_base/brennenstuhl_protocol.cpp b/esphome/components/remote_base/brennenstuhl_protocol.cpp new file mode 100644 index 0000000000..35261dc00a --- /dev/null +++ b/esphome/components/remote_base/brennenstuhl_protocol.cpp @@ -0,0 +1,144 @@ +#include "brennenstuhl_protocol.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::remote_base { + +static const char *const TAG = "remote.brennenstuhl"; + +// receiver timing ranges [µs] +constexpr uint32_t START_PULSE_MIN = 200; +constexpr uint32_t START_PULSE_MAX = 500; +constexpr uint32_t START_SYMBOL_MIN = 2600; +constexpr uint32_t START_SYMBOL_MAX = 2700; +constexpr uint32_t DATA_SYMBOL_MIN = 1500; +constexpr uint32_t DATA_SYMBOL_MAX = 1600; + +// transmitter timings [µs] +constexpr uint32_t PW_SHORT_US = 390; +constexpr uint32_t PW_LONG_US = 1160; +constexpr uint32_t PW_START_US = 2300; + +// number of data bits +constexpr uint32_t N_BITS = 24; + +// number of required symbols = 2 x (start + N_BITS) = 50 +constexpr uint32_t N_SYMBOLS_REQ = 2u * (N_BITS + 1); + +// number of bs codes within received frame +constexpr int32_t N_FRAME_CODES = 4; + +// decoder finite-state-machine +enum class RxSt { START_PULSE, START_SYMBOL, PULSE, DATA_SYMBOL }; + +// The encode() member function reserves and fills a complete frame, to be send. The Brennenstuhl +// RC receivers demand a frame with a start-symbol followed by 4 repeated codes. +void BrennenstuhlProtocol::encode(RemoteTransmitData *dst, const BrennenstuhlData &data) { + uint32_t code = data.code; + dst->reserve((N_SYMBOLS_REQ * N_FRAME_CODES) + 1); + for (int32_t kc = 0; kc != N_FRAME_CODES; kc++) { + dst->item(PW_SHORT_US, PW_START_US); + for (int32_t ic = (N_BITS - 1); ic != -1; ic--) { + if ((code >> ic) & 1) { + dst->item(PW_LONG_US, PW_SHORT_US); + } else { + dst->item(PW_SHORT_US, PW_LONG_US); + } + } + } +} + +// The decode() member function extracts Brennenstuhl codes from the received frame. Instead +// of validating the pulse width of the carriers and pauses individually, it is more accurate +// to validate the symbols (symbol=carrier+pause) The symbol pulsewidth is around 1550µs, but +// the pulse with of the carrier and the pauses vary greatly. Once the symbol pulsewidth is +// valid, a code bit becomes "1" if the carrier is longer then the pause and "0" else. A total +// frame consists of a start symbol and up to four codes. The decoder decodes all codes and +// returns the best code (the one with the most identical codes) +optional BrennenstuhlProtocol::decode(RemoteReceiveData src) { + uint32_t n_received = static_cast(src.size()); + BrennenstuhlData data{ + .code = 0, + }; + // suppress noisy frames, at least a complete bs_code should be available + if (n_received > N_SYMBOLS_REQ) { + uint32_t bs_codes[4] = {0, 0, 0, 0}; // internal codes + int32_t bs_cnt = 0; // number of bs codes found within frame + int32_t bs_idx = -1; // index to best bs code + uint32_t bit_cnt = 0; // bit counter [0..23] + uint32_t pw_pre = 0; // pulsewidth of previous carrier (abs value) + RxSt fsm = RxSt::START_PULSE; + for (uint32_t ic = 0; (ic != n_received) && (bs_cnt != N_FRAME_CODES); ic++) { + uint32_t pw_cur = (uint32_t) (src[ic] < 0 ? -src[ic] : src[ic]); // current pulsewidth + uint32_t pw_sym = pw_cur + pw_pre; // symbol=pulse+pause + switch (fsm) { + case RxSt::START_PULSE: { // check if start pulse is valid + if ((src[ic] > 0) && (pw_cur >= START_PULSE_MIN) && (pw_cur <= START_PULSE_MAX)) { + bs_codes[bs_cnt] = 0; + bit_cnt = 0; + pw_pre = pw_cur; + fsm = RxSt::START_SYMBOL; + } + break; + } + case RxSt::START_SYMBOL: { // check if start symbol is valid + if ((src[ic] < 0) && (pw_sym >= START_SYMBOL_MIN) && (pw_sym <= START_SYMBOL_MAX)) { + fsm = RxSt::PULSE; + } else { + fsm = RxSt::START_PULSE; + } + break; + } + case RxSt::PULSE: { // just grab pulse, validation is done in DATA_SYMBOL state + if (src[ic] > 0) { + pw_pre = pw_cur; + fsm = RxSt::DATA_SYMBOL; + } else { + fsm = RxSt::START_PULSE; + } + break; + } + case RxSt::DATA_SYMBOL: { // check if data symbol is valid and append bit to data + if ((src[ic] < 0) && (pw_sym >= DATA_SYMBOL_MIN) && (pw_sym <= DATA_SYMBOL_MAX)) { + bs_codes[bs_cnt] <<= 1; + bs_codes[bs_cnt] += (pw_cur < pw_pre) ? 1 : 0; + if (++bit_cnt < N_BITS) { + fsm = RxSt::PULSE; + } else { + bs_cnt++; // complete code found + fsm = RxSt::START_PULSE; // start over for further codes in frame + } + } else { + fsm = RxSt::START_PULSE; // decoding failed, start over for further codes + } + break; + } + } + } + if (bs_cnt > 0) { // complete codes found, find best code in list now + int32_t identical_max = 0; + for (int32_t ic = 0; ic != bs_cnt; ic++) { + int32_t identical_cnt = 0; + for (int32_t jc = 0; jc != bs_cnt; jc++) { + identical_cnt += (bs_codes[ic] == bs_codes[jc]) ? 1 : 0; + } + if (identical_cnt > identical_max) { + identical_max = identical_cnt; + bs_idx = ic; // save index to best code + } + } + if (bs_idx > -1) { + data.code = bs_codes[bs_idx]; + return data; // return best bs code of list + } + } + } + return {}; +} + +void BrennenstuhlProtocol::dump(const BrennenstuhlData &data) { + ESP_LOGI(TAG, "Brennenstuhl: code=0x%06" PRIx32, data.code); +} + +} // namespace esphome::remote_base diff --git a/esphome/components/remote_base/brennenstuhl_protocol.h b/esphome/components/remote_base/brennenstuhl_protocol.h new file mode 100644 index 0000000000..1d5b621714 --- /dev/null +++ b/esphome/components/remote_base/brennenstuhl_protocol.h @@ -0,0 +1,34 @@ +#pragma once + +#include "remote_base.h" + +#include + +namespace esphome::remote_base { + +struct BrennenstuhlData { + uint32_t code; + bool operator==(const BrennenstuhlData &rhs) const { return code == rhs.code; } +}; + +class BrennenstuhlProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const BrennenstuhlData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Brennenstuhl) + +template class BrennenstuhlAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, code) + + void encode(RemoteTransmitData *dst, Ts... x) override { + BrennenstuhlData data{}; + data.code = this->code_.value(x...); + BrennenstuhlProtocol().encode(dst, data); + } +}; + +} // namespace esphome::remote_base diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 71e5f1488c..0bb1811069 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed +from . import boards from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns # force import gpio to register pin schema @@ -35,6 +36,23 @@ AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True +def get_board() -> str: + """Return the configured board name.""" + return CORE.data[KEY_RP2040][KEY_BOARD] + + +def board_has_wifi() -> bool: + """Return True if the configured board has WiFi (CYW43 wireless chip). + + Returns True for unknown/custom boards to avoid rejecting valid + configurations for boards not in the generated list. + """ + board_info = boards.BOARDS.get(get_board()) + if board_info is None: + return True + return board_info.get("wifi", False) + + def set_core_data(config): CORE.data[KEY_RP2040] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_RP2040 diff --git a/esphome/components/rp2040/boards.py b/esphome/components/rp2040/boards.py index c99934567a..aac12eae5a 100644 --- a/esphome/components/rp2040/boards.py +++ b/esphome/components/rp2040/boards.py @@ -1910,6 +1910,7 @@ BOARDS = { "name": "Pimoroni PicoPlus2W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "pimoroni_plasma2040": { @@ -1926,6 +1927,7 @@ BOARDS = { "name": "Pimoroni Plasma2350W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "pimoroni_servo2040": { "name": "Pimoroni Servo2040", @@ -1976,12 +1978,14 @@ BOARDS = { "name": "Raspberry Pi Pico 2W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "rpipicow": { "name": "Raspberry Pi Pico W", "mcu": "rp2040", "max_pin": 29, + "wifi": True, "max_virtual_pin": 64, }, "sea_picro": { @@ -2013,6 +2017,7 @@ BOARDS = { "name": "Soldered Electronics NULA RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "solderparty_rp2040_stamp": { "name": "Solder Party RP2040 Stamp", @@ -2038,6 +2043,7 @@ BOARDS = { "name": "SparkFun IoT RedBoard RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "sparkfun_micromodrp2040": { "name": "SparkFun MicroMod RP2040", @@ -2063,18 +2069,21 @@ BOARDS = { "name": "SparkFun Thing Plus RP2350", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "sparkfun_xrp_controller": { "name": "SparkFun XRP Controller", "mcu": "rp2350", "max_pin": 47, + "wifi": True, "max_virtual_pin": 64, }, "sparkfun_xrp_controller_beta": { "name": "SparkFun XRP Controller (Beta)", "mcu": "rp2040", "max_pin": 29, + "wifi": True, "max_virtual_pin": 64, }, "upesy_rp2040_devkit": { @@ -2161,6 +2170,7 @@ BOARDS = { "name": "Waveshare RP2350B Plus W", "mcu": "rp2350", "max_pin": 47, + "wifi": True, }, "wiznet_5100s_evb_pico": { "name": "WIZnet W5100S-EVB-Pico", diff --git a/esphome/components/rp2040/generate_boards.py b/esphome/components/rp2040/generate_boards.py index 7ea02d185e..8af261396c 100644 --- a/esphome/components/rp2040/generate_boards.py +++ b/esphome/components/rp2040/generate_boards.py @@ -78,11 +78,17 @@ def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]: display_name = f"{vendor} {name}".strip() if vendor else name - boards[board_name] = { + extra_flags = build.get("extra_flags", "") + has_wifi = "PICO_CYW43_SUPPORTED=1" in extra_flags + + board_entry: dict = { "name": display_name, "mcu": mcu, "max_pin": MCU_MAX_PIN.get(mcu, DEFAULT_MAX_PIN), } + if has_wifi: + board_entry["wifi"] = True + boards[board_name] = board_entry # Get pins for this variant if variant not in variant_pins_cache: diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index ad69192af9..8cb5f7c18d 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -10,21 +10,13 @@ #include // For cyw43_arch_lwip_begin/end (LwIPLock) #elif defined(USE_ETHERNET) #include // For ethernet_arch_lwip_begin/end (LwIPLock) +#include "esphome/components/ethernet/ethernet_component.h" #endif #include #include namespace esphome { -uint32_t random_uint32() { - uint32_t result = 0; - for (uint8_t i = 0; i < 32; i++) { - result <<= 1; - result |= rosc_hw->randombit; - } - return result; -} - bool random_bytes(uint8_t *data, size_t len) { while (len-- != 0) { uint8_t result = 0; @@ -71,6 +63,8 @@ LwIPLock::~LwIPLock() {} void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #ifdef USE_WIFI WiFi.macAddress(mac); +#elif defined(USE_ETHERNET) + ethernet::global_eth_component->get_eth_mac_address_raw(mac); #endif } diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index 0a91136a9f..cfc802b28f 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -14,7 +14,7 @@ namespace esphome::rp2040 { -static const char *const TAG = "rp2040.preferences"; +static const char *const TAG = "preferences"; static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512; diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index 3ae35cc5f1..8db69aa53e 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -181,7 +181,8 @@ async def process_runtime_image_config(config: dict) -> RuntimeImageSettings: transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE")) # Get byte order (True for big endian, False for little endian) - byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN" + # If unspecified, use little endian + byte_order_big_endian = config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN" # Get placeholder if specified placeholder = None diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 5a4a2ea7d6..2ebe67c3a5 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -106,7 +106,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) { break; } case image::IMAGE_TYPE_RGB565: { - uint32_t pos = this->get_position_(x, y); + const size_t pos = (x + y * this->buffer_width_) * 2; Color mapped_color = color; this->map_chroma_key(mapped_color); uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color); @@ -118,7 +118,8 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) { this->buffer_[pos + 1] = static_cast((rgb565 >> 8) & 0xFF); } if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 2] = color.w; + const size_t alpha_pos = pos / 2 + this->buffer_width_ * this->buffer_height_ * 2; + this->buffer_[alpha_pos] = color.w; } break; } @@ -283,6 +284,10 @@ size_t RuntimeImage::resize_buffer_(int width, int height) { } size_t RuntimeImage::get_buffer_size_(int width, int height) const { + if (this->get_type() == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + // Add extra alpha channel for RGB565 with alpha + return width * height * 3; + } return (this->get_bpp() * width + 7u) / 8u * height; } diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index f235e4e68c..74ca2ce39a 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -5,6 +5,30 @@ namespace esphome { namespace sdl { +int Sdl::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int Sdl::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + void Sdl::setup() { SDL_Init(SDL_INIT_VIDEO); this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, @@ -49,6 +73,19 @@ void Sdl::draw_pixel_at(int x, int y, Color color) { if (!this->get_clipping().inside(x, y)) return; + if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { + x = this->width_ - x - 1; + y = this->height_ - y - 1; + } else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) { + auto tmp = x; + x = this->width_ - y - 1; + y = tmp; + } else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + auto tmp = y; + y = this->height_ - x - 1; + x = tmp; + } + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index c025e8ff6e..ce34cb817e 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -33,8 +33,8 @@ class Sdl : public display::Display { this->pos_x_ = pos_x; this->pos_y_ = pos_y; } - int get_width() override { return this->width_; } - int get_height() override { return this->height_; } + int get_width() override; + int get_height() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } void dump_config() override { LOG_DISPLAY("", "SDL", this); } template void add_key_listener(int32_t keycode, F &&callback) { diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 00d822b75c..f3c256c62a 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -165,7 +165,7 @@ uint32_t SerialProxy::get_modem_pins() const { (this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u); } -uart::FlushResult SerialProxy::flush_port() { +uart::UARTFlushResult SerialProxy::flush_port() { ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_); return this->flush(); } diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 5adfa4fe53..c435787a61 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -92,7 +92,7 @@ class SerialProxy : public uart::UARTDevice, public Component { uint32_t get_modem_pins() const; /// Flush the serial port (block until all TX data is sent) - uart::FlushResult flush_port(); + uart::UARTFlushResult flush_port(); /// Set the RTS GPIO pin (from YAML configuration) void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 42be326202..b1dbde22a4 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -1,4 +1,5 @@ #include "sht4x.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { @@ -9,14 +10,12 @@ static const char *const TAG = "sht4x"; static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; -void SHT4XComponent::start_heater_() { - uint8_t cmd[] = {this->heater_command_}; - - ESP_LOGD(TAG, "Heater turning on"); - if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error(LOG_STR("Failed to turn on heater")); - } -} +// Conversion constants from SHT4x datasheet +static constexpr float TEMPERATURE_OFFSET = -45.0f; +static constexpr float TEMPERATURE_SPAN = 175.0f; +static constexpr float HUMIDITY_OFFSET = -6.0f; +static constexpr float HUMIDITY_SPAN = 125.0f; +static constexpr float RAW_MAX = 65535.0f; void SHT4XComponent::read_serial_number_() { uint16_t buffer[2]; @@ -39,8 +38,8 @@ void SHT4XComponent::setup() { this->read_serial_number_(); if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) { - uint32_t heater_interval = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); - ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); + this->heater_interval_ = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); + ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_); if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { @@ -62,8 +61,6 @@ void SHT4XComponent::setup() { } } ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); - - this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this)); } } @@ -106,19 +103,27 @@ void SHT4XComponent::update() { // Evaluate and publish measurements if (this->temp_sensor_ != nullptr) { // Temp is contained in the first result word - float sensor_value_temp = buffer[0]; - float temp = -45 + 175 * sensor_value_temp / 65535; - + float temp = TEMPERATURE_OFFSET + TEMPERATURE_SPAN * static_cast(buffer[0]) / RAW_MAX; this->temp_sensor_->publish_state(temp); } if (this->humidity_sensor_ != nullptr) { // Relative humidity is in the second result word - float sensor_value_rh = buffer[1]; - float rh = -6 + 125 * sensor_value_rh / 65535; - + float rh = HUMIDITY_OFFSET + HUMIDITY_SPAN * static_cast(buffer[1]) / RAW_MAX; this->humidity_sensor_->publish_state(rh); } + + // Fire heater after measurement to maximize cooldown time before the next reading. + // The heater command produces a measurement that we don't need (datasheet 4.9). + if (this->heater_interval_ > 0) { + uint32_t now = millis(); + if (now - this->last_heater_millis_ >= this->heater_interval_) { + ESP_LOGD(TAG, "Heater turning on"); + if (this->write_command(this->heater_command_)) { + this->last_heater_millis_ = now; + } + } + } }); } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index aec0f3d7f8..51f473fe3f 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -35,9 +35,10 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri SHT4XHEATERTIME heater_time_; float duty_cycle_; - void start_heater_(); void read_serial_number_(); uint8_t heater_command_; + uint32_t heater_interval_{0}; + uint32_t last_heater_millis_{0}; uint32_t serial_number_; sensor::Sensor *temp_sensor_{nullptr}; diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index c790719816..7303cea96b 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -142,7 +142,7 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s /// On ESP8266, uses esp_delay() with a callback that checks socket activity. /// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt /// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. -void socket_delay(uint32_t ms); +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) /// Signal socket/IO activity and wake the main loop early. /// On ESP8266: sets flag + esp_schedule(). diff --git a/esphome/components/spa06_base/__init__.py b/esphome/components/spa06_base/__init__.py new file mode 100644 index 0000000000..97d09aad81 --- /dev/null +++ b/esphome/components/spa06_base/__init__.py @@ -0,0 +1,201 @@ +import math + +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PASCAL, +) + +CODEOWNERS = ["@danielkent-net"] + +spa06_ns = cg.esphome_ns.namespace("spa06_base") + +SampleRate = spa06_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "1": SampleRate.SAMPLE_RATE_1, + "2": SampleRate.SAMPLE_RATE_2, + "4": SampleRate.SAMPLE_RATE_4, + "8": SampleRate.SAMPLE_RATE_8, + "16": SampleRate.SAMPLE_RATE_16, + "32": SampleRate.SAMPLE_RATE_32, + "64": SampleRate.SAMPLE_RATE_64, + "128": SampleRate.SAMPLE_RATE_128, + "25p16": SampleRate.SAMPLE_RATE_25P16, + "25p8": SampleRate.SAMPLE_RATE_25P8, + "25p4": SampleRate.SAMPLE_RATE_25P4, + "25p2": SampleRate.SAMPLE_RATE_25P2, + "25": SampleRate.SAMPLE_RATE_25, + "50": SampleRate.SAMPLE_RATE_50, + "100": SampleRate.SAMPLE_RATE_100, + "200": SampleRate.SAMPLE_RATE_200, +} + +Oversampling = spa06_ns.enum("Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": Oversampling.OVERSAMPLING_NONE, + "2X": Oversampling.OVERSAMPLING_X2, + "4X": Oversampling.OVERSAMPLING_X4, + "8X": Oversampling.OVERSAMPLING_X8, + "16X": Oversampling.OVERSAMPLING_X16, + "32X": Oversampling.OVERSAMPLING_X32, + "64X": Oversampling.OVERSAMPLING_X64, + "128X": Oversampling.OVERSAMPLING_X128, +} + +SPA06Component = spa06_ns.class_("SPA06Component", cg.PollingComponent) + + +def spa_oversample_time(oversample): + # Pressure oversampling conversion times are listed on datasheet Pg. 26 + # Datasheet does not have a table for temperature oversampling; + # assumption is that it is the same as pressure + OVERSAMPLING_CONVERSION_TIMES = { + "NONE": 3.6, + "2X": 5.2, + "4X": 8.4, + "8X": 14.8, + "16X": 27.6, + "32X": 53.2, + "64X": 104.4, + "128X": 206.8, + } + return OVERSAMPLING_CONVERSION_TIMES[oversample] + + +def spa_sample_rate(rate): + SAMPLE_RATE_OPTIONS_HZ = { + "1": 1.0, + "2": 2.0, + "4": 4.0, + "8": 8.0, + "16": 16.0, + "32": 32.0, + "64": 64.0, + "128": 128.0, + "25p16": 25.0 / 16.0, + "25p8": 25.0 / 8.0, + "25p4": 25.0 / 4.0, + "25p2": 25.0 / 2.0, + "25": 25.0, + "50": 50.0, + "100": 100.0, + "200": 200.0, + } + return SAMPLE_RATE_OPTIONS_HZ[rate] + + +def compute_measurement_conversion_time(config): + # - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet + # - returns a rounded up time in ms + + # No conversion time necessary without a pressure sensor + pressure_conversion_time = 0.0 + if pressure_config := config.get(CONF_PRESSURE): + pressure_conversion_time = spa_oversample_time( + pressure_config.get(CONF_OVERSAMPLING) + ) + # Temperature required in all cases, default to minimum sample time + temperature_conversion_time = 3.6 + if temperature_config := config.get(CONF_TEMPERATURE): + temperature_conversion_time = spa_oversample_time( + temperature_config.get(CONF_OVERSAMPLING) + ) + + # TODO: Read datasheet to find conversion time error + return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time)) + + +def measurement_timing_check(config): + + temp_time = 0.0 + if temperature_config := config.get(CONF_TEMPERATURE): + temp_oss = ( + spa_oversample_time(temperature_config.get(CONF_OVERSAMPLING)) / 1000.0 + ) + temp_hz = spa_sample_rate(temperature_config.get(CONF_SAMPLE_RATE)) + temp_time = temp_oss * temp_hz + + pres_time = 0.0 + if pressure_config := config.get(CONF_PRESSURE): + pres_oss = spa_oversample_time(pressure_config.get(CONF_OVERSAMPLING)) / 1000.0 + pres_hz = spa_sample_rate(pressure_config.get(CONF_SAMPLE_RATE)) + pres_time = pres_oss * pres_hz + + if temp_time + pres_time >= 1: + raise cv.Invalid( + "Combined sample_rate and oversampling for temperature and pressure is too high" + ) + return config + + +CONFIG_SCHEMA_BASE = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SPA06Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="1"): cv.enum( + SAMPLE_RATE_OPTIONS, lower=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_PASCAL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="1"): cv.enum( + SAMPLE_RATE_OPTIONS, lower=True + ), + } + ), + }, +).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA_BASE.add_extra(measurement_timing_check) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add( + var.set_temperature_oversampling_config( + temperature_config[CONF_OVERSAMPLING] + ) + ) + cg.add( + var.set_temperature_sample_rate_config(temperature_config[CONF_SAMPLE_RATE]) + ) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING])) + cg.add(var.set_pressure_sample_rate_config(pressure_config[CONF_SAMPLE_RATE])) + + cg.add(var.set_conversion_time(compute_measurement_conversion_time(config))) + return var diff --git a/esphome/components/spa06_base/spa06_base.cpp b/esphome/components/spa06_base/spa06_base.cpp new file mode 100644 index 0000000000..36268aa9a2 --- /dev/null +++ b/esphome/components/spa06_base/spa06_base.cpp @@ -0,0 +1,320 @@ +#include "spa06_base.h" + +#include "esphome/core/helpers.h" + +namespace esphome::spa06_base { + +static const char *const TAG = "spa06"; + +// Sign extension function for <=16 bit types +inline int16_t decode16(uint8_t msb, uint8_t lsb, size_t bits, size_t head = 0) { + return static_cast(encode_uint16(msb, lsb) << head) >> (16 - bits); +} + +// Sign extension function for <=32 bit types +inline int32_t decode32(uint8_t xmsb, uint8_t msb, uint8_t lsb, uint8_t xlsb, size_t bits, size_t head = 0) { + return static_cast(encode_uint32(xmsb, msb, lsb, xlsb) << head) >> (32 - bits); +} + +void SPA06Component::dump_config() { + ESP_LOGCONFIG(TAG, "SPA06:"); + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_); + if (this->temperature_sensor_) { + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, + " Oversampling: %s\n" + " Rate: %s", + LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)), + LOG_STR_ARG(meas_rate_to_str(this->temperature_rate_))); + } + if (this->pressure_sensor_) { + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, + " Oversampling: %s\n" + " Rate: %s", + LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)), + LOG_STR_ARG(meas_rate_to_str(this->pressure_rate_))); + } +} + +void SPA06Component::setup() { + // Startup sequence for SPA06 (Pg. 16, Figure 4.6.4): + // 1. Perform a soft reset + // 2. Verify sensor chip ID matches + // 3. Verify coefficients are ready + // 4. Read coefficients + // 5. Configure temperature and pressure sensors + // 6. Write communication settings + // 7. Write measurement settings (background measurement mode) + + // 1. Soft reset + if (!this->soft_reset_()) { + this->mark_failed(LOG_STR("Reset failed")); + return; + } + + // soft_reset_() internally delays by 3ms to make sure that + // the sensor is in a ready state and coefficients are ready. + + // 2. Read chip ID + // TODO: check ID for consistency? + if (!spa_read_byte(SPA06_ID, &this->prod_id_.reg)) { + this->mark_failed(LOG_STR("Chip ID read failure")); + return; + } + ESP_LOGV(TAG, + "Product Info:\n" + " Prod ID: %u\n" + " Rev ID: %u", + this->prod_id_.bit.prod_id, this->prod_id_.bit.rev_id); + + // 3. Read chip readiness from MEAS_CFG + // First check if the sensor reports ready + if (!spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) { + this->mark_failed(LOG_STR("Sensor status read failure")); + return; + } + // Check if the sensor reports coefficients are ready + if (!meas_.bit.coef_ready) { + this->mark_failed(LOG_STR("Coefficients not ready")); + return; + } + + // 4. Read coefficients + if (!this->read_coefficients_()) { + this->mark_failed(LOG_STR("Coefficients read error")); + return; + } + + // 5. Configure temperature and pressure sensors + // Default to measuring both temperature and pressure + + // Temperature must be read regardless of configuration to compute pressure + // If temperature is not configured in config: + // - No oversampling is used + // - Lowest possible rate is configured + if (!this->temperature_sensor_) { + this->temperature_rate_ = SAMPLE_RATE_1; + this->temperature_oversampling_ = OVERSAMPLING_NONE; + this->kt_ = oversampling_to_scale_factor(OVERSAMPLING_NONE); + } + + // If pressure is not configured in config + // - No oversampling is used + // - Lowest possible rate is configured + if (!this->pressure_sensor_) { + this->pressure_rate_ = SAMPLE_RATE_1; + this->pressure_oversampling_ = OVERSAMPLING_NONE; + this->kp_ = oversampling_to_scale_factor(OVERSAMPLING_NONE); + } + + // Write temperature settings + if (!write_temperature_settings_(this->temperature_oversampling_, this->temperature_rate_)) { + this->mark_failed(LOG_STR("Temperature settings write fail")); + return; + } + + // Write pressure settings + if (!write_pressure_settings_(this->pressure_oversampling_, this->pressure_rate_)) { + this->mark_failed(LOG_STR("Pressure settings write fail")); + return; + } + // 6. Write communication settings + // This call sets the bit shifts for pressure and temperature if + // their respective oversampling config is > X8 + // This call also disables interrupts, FIFO, and specifies SPI 4-wire + if (!write_communication_settings_(this->pressure_oversampling_ > OVERSAMPLING_X8, + this->temperature_oversampling_ > OVERSAMPLING_X8)) { + this->mark_failed(LOG_STR("Comm settings write fail")); + return; + } + + // 7. Write measurement settings + // This function sets background measurement mode without FIFO + if (!write_measurement_settings_(this->pressure_sensor_ ? MeasCrtl::MEASCRTL_BG_BOTH : MeasCrtl::MEASCRTL_BG_TEMP)) { + this->mark_failed(LOG_STR("Measurement settings write fail")); + return; + } +} + +bool SPA06Component::write_temperature_settings_(Oversampling oversampling, SampleRate rate) { + return this->write_sensor_settings_(oversampling, rate, SPA06_TMP_CFG); +} + +bool SPA06Component::write_pressure_settings_(Oversampling oversampling, SampleRate rate) { + return this->write_sensor_settings_(oversampling, rate, SPA06_PSR_CFG); +} + +bool SPA06Component::write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg) { + if (reg != SPA06_PSR_CFG && reg != SPA06_TMP_CFG) { + return false; + } + this->pt_meas_cfg_.bit.rate = rate; + this->pt_meas_cfg_.bit.prc = oversampling; + ESP_LOGD(TAG, "Config write: %02x", this->pt_meas_cfg_.reg); + return spa_write_byte(reg, this->pt_meas_cfg_.reg); +} + +bool SPA06Component::write_measurement_settings_(MeasCrtl crtl) { + this->meas_.bit.meas_crtl = crtl; + return spa_write_byte(SPA06_MEAS_CFG, this->meas_.reg); +} + +bool SPA06Component::write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl, + bool interrupt_fifo, bool interrupt_tmp, bool interrupt_prs, + bool enable_fifo, bool spi_3wire) { + this->cfg_.bit.p_shift = pressure_shift; + this->cfg_.bit.t_shift = temperature_shift; + this->cfg_.bit.int_hl = interrupt_hl; + this->cfg_.bit.int_fifo = interrupt_fifo; + this->cfg_.bit.int_tmp = interrupt_tmp; + this->cfg_.bit.int_prs = interrupt_prs; + this->cfg_.bit.fifo_en = enable_fifo; + this->cfg_.bit.spi_3wire = spi_3wire; + return spa_write_byte(SPA06_CFG_REG, this->cfg_.reg); +} + +bool SPA06Component::read_coefficients_() { + uint8_t coef[SPA06_COEF_LEN]; + if (!spa_read_bytes(SPA06_COEF, coef, SPA06_COEF_LEN)) { + return false; + } + this->c0_ = decode16(coef[0], coef[1], 12); + this->c1_ = decode16(coef[1], coef[2], 12, 4); + this->c00_ = decode32(coef[3], coef[4], coef[5], 0, 20); + this->c10_ = decode32(coef[5], coef[6], coef[7], 0, 20, 4); + this->c01_ = decode16(coef[8], coef[9], 16); + this->c11_ = decode16(coef[10], coef[11], 16); + this->c20_ = decode16(coef[12], coef[13], 16); + this->c21_ = decode16(coef[14], coef[15], 16); + this->c30_ = decode16(coef[16], coef[17], 16); + this->c31_ = decode16(coef[18], coef[19], 12); + this->c40_ = decode16(coef[19], coef[20], 12, 4); + + ESP_LOGV(TAG, + "Coefficients:\n" + " c0: %i, c1: %i,\n" + " c00: %i, c10: %i, c20: %i, c30: %i, c40: %i,\n" + " c01: %i, c11: %i, c21: %i, c31: %i", + this->c0_, this->c1_, this->c00_, this->c10_, this->c20_, this->c30_, this->c40_, this->c01_, this->c11_, + this->c21_, this->c31_); + return true; +} + +bool SPA06Component::soft_reset_() { + // Setup steps for SPA06: + // 1. Perform a protocol reset (required to write command for SPI code, noop for I2C) + this->protocol_reset(); + + // 2. Perform the actual reset + this->reset_.bit.fifo_flush = true; + this->reset_.bit.soft_rst = SPA06_SOFT_RESET; + if (!this->spa_write_byte(SPA06_RESET, this->reset_.reg)) { + return false; + } + + // 3. Wait for chip to become ready. Datasheet specifies 2 ms; wait 3 + delay(3); + // 4. Perform another protocol reset (required for SPI code, noop for I2C) + this->protocol_reset(); + return true; +} + +// Temperature conversion formula. See datasheet pg. 14 +float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5 + this->c1_ * t_raw_sc; } +// Pressure conversion formula. See datasheet pg. 14 +float SPA06Component::convert_pressure_(const float &p_raw_sc, const float &t_raw_sc) { + float p2_raw_sc = p_raw_sc * p_raw_sc; + float p3_raw_sc = p2_raw_sc * p_raw_sc; + float p4_raw_sc = p3_raw_sc * p_raw_sc; + return this->c00_ + (float) this->c10_ * p_raw_sc + (float) this->c20_ * p2_raw_sc + (float) this->c30_ * p3_raw_sc + + (float) this->c40_ * p4_raw_sc + + t_raw_sc * ((float) this->c01_ + (float) this->c11_ * p_raw_sc + (float) this->c21_ * p2_raw_sc + + (float) this->c31_ * p3_raw_sc); +} + +void SPA06Component::update() { + // Verify either a temperature or pressure sensor is defined before proceeding + if ((!this->temperature_sensor_) && (!this->pressure_sensor_)) { + return; + } + + // Queue a background task for retrieving the measurement + this->set_timeout("measurement", this->conversion_time_, [this]() { + float raw_temperature; + float temperature = 0.0; + float pressure = 0.0; + + // Check measurement register for readiness + if (!this->spa_read_byte(SPA06_MEAS_CFG, &this->meas_.reg)) { + ESP_LOGW(TAG, "Cannot read meas config"); + this->status_set_warning(); + return; + } + if (this->pressure_sensor_) { + if (!this->meas_.bit.prs_ready || !this->meas_.bit.tmp_ready) { + ESP_LOGW(TAG, "Temperature and pressure not ready"); + this->status_set_warning(); + return; + } + if (!this->read_temperature_and_pressure_(temperature, pressure, raw_temperature)) { + ESP_LOGW(TAG, "Temperature and pressure read failure"); + this->status_set_warning(); + return; + } + } else { + if (!this->meas_.bit.tmp_ready) { + ESP_LOGW(TAG, "Temperature not ready"); + this->status_set_warning(); + return; + } + if (!this->read_temperature_(temperature, raw_temperature)) { + ESP_LOGW(TAG, "Temperature read fail"); + this->status_set_warning(); + return; + } + } + if (this->temperature_sensor_) { + this->temperature_sensor_->publish_state(temperature); + } else { + ESP_LOGV(TAG, "No temperature sensor configured"); + } + if (this->pressure_sensor_) { + this->pressure_sensor_->publish_state(pressure); + } else { + ESP_LOGV(TAG, "No pressure sensor configured"); + } + this->status_clear_warning(); + }); +} + +bool SPA06Component::read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc) { + // Temperature read and decode + if (!this->read_temperature_(temperature, t_raw_sc)) { + return false; + } + // Read raw pressure from device + uint8_t buf[3]; + if (!this->spa_read_bytes(SPA06_PSR, buf, 3)) { + return false; + } + // Calculate raw scaled pressure value + float p_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kp_; + + // Calculate full pressure values + pressure = this->convert_pressure_(p_raw_sc, t_raw_sc); + return true; +} + +bool SPA06Component::read_temperature_(float &temperature, float &t_raw_sc) { + uint8_t buf[3]; + if (!this->spa_read_bytes(SPA06_TMP, buf, 3)) { + return false; + } + + t_raw_sc = (float) decode32(buf[0], buf[1], buf[2], 0, 24) / (float) this->kt_; + temperature = this->convert_temperature_(t_raw_sc); + return true; +} +} // namespace esphome::spa06_base diff --git a/esphome/components/spa06_base/spa06_base.h b/esphome/components/spa06_base/spa06_base.h new file mode 100644 index 0000000000..5239e80ec5 --- /dev/null +++ b/esphome/components/spa06_base/spa06_base.h @@ -0,0 +1,257 @@ +// SPA06 interface code for ESPHome +// All datasheet page references refer to Goermicro SPA06-003 datasheet version 2.0 + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/progmem.h" + +namespace esphome::spa06_base { + +// Read sizes. All other registers are size 1 +constexpr size_t SPA06_MEAS_LEN = 3; +constexpr size_t SPA06_COEF_LEN = 21; + +// Soft reset command (0b1001, 0x9) +constexpr uint8_t SPA06_SOFT_RESET = 0x9; + +// SPA06 Register Addresses +enum Register : uint8_t { + SPA06_PSR = 0x00, // Pressure Reading MSB (or all 3) + SPA06_PSR_B1 = 0x01, // Pressure Reading LSB + SPA06_PSR_B0 = 0x02, // Pressure Reading XLSB (LSB: Pressure flag in FIFO) + SPA06_TMP = 0x03, // Temperature Reading MSB (or all 3) + SPA06_TMP_B1 = 0x04, // Temperature Reading LSB + SPA06_TMP_B0 = 0x05, // Temperature Reading XLSB + SPA06_PSR_CFG = 0x06, // Pressure Configuration + SPA06_TMP_CFG = 0x07, // Temperature Configuration + SPA06_MEAS_CFG = 0x08, // Measurement Configuration (includes readiness) + SPA06_CFG_REG = 0x09, // Configuration Register + SPA06_INT_STS = 0x0A, // Interrupt Status + SPA06_FIFO_STS = 0x0B, // FIFO Status + SPA06_RESET = 0x0C, // Reset + FIFO Flush + SPA06_ID = 0x0D, // Product ID and revision + SPA06_COEF = 0x10, // Coefficients (0x10-0x24) + SPA06_INVALID_CMD = 0x25, // End of enum command +}; + +// Oversampling config. +enum Oversampling : uint8_t { + OVERSAMPLING_NONE = 0x0, + OVERSAMPLING_X2 = 0x1, + OVERSAMPLING_X4 = 0x2, + OVERSAMPLING_X8 = 0x3, + OVERSAMPLING_X16 = 0x4, + OVERSAMPLING_X32 = 0x5, + OVERSAMPLING_X64 = 0x6, + OVERSAMPLING_X128 = 0x7, + OVERSAMPLING_COUNT = 0x8, +}; + +// Measuring rate config +enum SampleRate : uint8_t { + SAMPLE_RATE_1 = 0x0, + SAMPLE_RATE_2 = 0x1, + SAMPLE_RATE_4 = 0x2, + SAMPLE_RATE_8 = 0x3, + SAMPLE_RATE_16 = 0x4, + SAMPLE_RATE_32 = 0x5, + SAMPLE_RATE_64 = 0x6, + SAMPLE_RATE_128 = 0x7, + SAMPLE_RATE_25P16 = 0x8, + SAMPLE_RATE_25P8 = 0x9, + SAMPLE_RATE_25P4 = 0xA, + SAMPLE_RATE_25P2 = 0xB, + SAMPLE_RATE_25 = 0xC, + SAMPLE_RATE_50 = 0xD, + SAMPLE_RATE_100 = 0xE, + SAMPLE_RATE_200 = 0xF, +}; + +// Measuring control config, set in MEAS_CFG register. +// See datasheet pages 28-29 +enum MeasCrtl : uint8_t { + MEASCRTL_IDLE = 0x0, + MEASCRTL_PRES = 0x1, + MEASCRTL_TEMP = 0x2, + MEASCRTL_BG_PRES = 0x5, + MEASCRTL_BG_TEMP = 0x6, + MEASCRTL_BG_BOTH = 0x7, +}; + +// Oversampling scale factors. See datasheet page 15. +constexpr uint32_t OVERSAMPLING_K_LUT[8] = {524288, 1572864, 3670016, 7864320, 253952, 516096, 1040384, 2088960}; +PROGMEM_STRING_TABLE(MeasRateStrings, "1Hz", "2Hz", "4Hz", "8Hz", "16Hz", "32Hz", "64Hz", "128Hz", "1.5625Hz", + "3.125Hz", "6.25Hz", "12.5Hz", "25Hz", "50Hz", "100Hz", "200Hz"); +PROGMEM_STRING_TABLE(OversamplingStrings, "X1", "X2", "X4", "X8", "X16", "X32", "X64", "X128"); + +inline static const LogString *oversampling_to_str(const Oversampling oversampling) { + return OversamplingStrings::get_log_str(static_cast(oversampling), OversamplingStrings::LAST_INDEX); +} +inline static const LogString *meas_rate_to_str(SampleRate rate) { + return MeasRateStrings::get_log_str(static_cast(rate), MeasRateStrings::LAST_INDEX); +} +inline uint32_t oversampling_to_scale_factor(const Oversampling oversampling) { + return OVERSAMPLING_K_LUT[static_cast(oversampling)]; +}; + +class SPA06Component : public PollingComponent { + public: + //// Standard ESPHome component class functions + void setup() override; + void update() override; + void dump_config() override; + + //// ESPHome-side settings + void set_conversion_time(uint16_t conversion_time) { this->conversion_time_ = conversion_time; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } + void set_temperature_oversampling_config(Oversampling temperature_oversampling) { + this->temperature_oversampling_ = temperature_oversampling; + this->kt_ = oversampling_to_scale_factor(temperature_oversampling); + } + void set_pressure_oversampling_config(Oversampling pressure_oversampling) { + this->pressure_oversampling_ = pressure_oversampling; + this->kp_ = oversampling_to_scale_factor(pressure_oversampling); + } + void set_pressure_sample_rate_config(SampleRate rate) { this->pressure_rate_ = rate; } + void set_temperature_sample_rate_config(SampleRate rate) { this->temperature_rate_ = rate; } + + protected: + // Virtual read functions. Implemented in SPI/I2C components + virtual bool spa_read_byte(uint8_t reg, uint8_t *data) = 0; + virtual bool spa_write_byte(uint8_t reg, uint8_t data) = 0; + virtual bool spa_read_bytes(uint8_t reg, uint8_t *data, size_t len) = 0; + virtual bool spa_write_bytes(uint8_t reg, uint8_t *data, size_t len) = 0; + + //// Protocol-specific read functions + // Soft reset + bool soft_reset_(); + // Protocol-specific reset (used for SPI only, implemented as noop for I2C) + virtual void protocol_reset() {} + // Read temperature and calculate Celsius and scaled raw temperatures + bool read_temperature_(float &temperature, float &t_raw_sc); + // No pressure only read! Pressure calculation depends on scaled temperature value + // Read temperature and calculate Celsius temperature, Pascal pressure, and scaled raw temperature + bool read_temperature_and_pressure_(float &temperature, float &pressure, float &t_raw_sc); + // Read coefficients. Stores in class variables. + bool read_coefficients_(); + + //// Protocol-specific write functions + // Write temperature settings to TMP_CFG register + bool write_temperature_settings_(Oversampling oversampling, SampleRate rate); + // Write pressure settings to PRS_CFG register + bool write_pressure_settings_(Oversampling oversampling, SampleRate rate); + // Write measurement settings to MEAS_CRTL register + bool write_measurement_settings_(MeasCrtl crtl); + + // Write communication settings to CFG_REG register + // Set pressure_shift to true if pressure oversampling >X8 + // Set temperature_shift to true if temperature oversampling >X8 + bool write_communication_settings_(bool pressure_shift, bool temperature_shift, bool interrupt_hl = false, + bool interrupt_fifo = false, bool interrupt_tmp = false, + bool interrupt_prs = false, bool enable_fifo = false, bool spi_3wire = false); + + //// Protocol helper functions + // Write function for both temperature and pressure (deduplicates code) + bool write_sensor_settings_(Oversampling oversampling, SampleRate rate, uint8_t reg); + // Convert raw temperature reading into Celsius + float convert_temperature_(const float &t_raw_sc); + // Convert raw pressure and scaled raw temperature into Pascals + float convert_pressure_(const float &p_raw_sc, const float &t_raw_sc); + + //// Protocol-related variables + // Oversampling scale factors. Defaults are for X16 (pressure) and X1 (temp) + uint32_t kp_{253952}, kt_{524288}; + // Coefficients for calculating pressure and temperature from raw values + // Obtained from IC during setup + int32_t c00_{0}, c10_{0}; + int16_t c0_{0}, c1_{0}, c01_{0}, c11_{0}, c20_{0}, c21_{0}, c30_{0}, c31_{0}, c40_{0}; + + //// ESPHome class objects and configuration + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + Oversampling temperature_oversampling_{Oversampling::OVERSAMPLING_NONE}; + Oversampling pressure_oversampling_{Oversampling::OVERSAMPLING_X16}; + SampleRate temperature_rate_{SampleRate::SAMPLE_RATE_1}; + SampleRate pressure_rate_{SampleRate::SAMPLE_RATE_1}; + // Default conversion time: 27.6ms (16x pres) + 3.6ms (1x temp) ~ 32ms + uint16_t conversion_time_{32}; + + union { + struct { + Oversampling prc : 4; + SampleRate rate : 4; + } bit; + uint8_t reg; + } pt_meas_cfg_ = {.reg = 0}; // PRS_CFG and TMP_CFG + + union { + struct { + uint8_t meas_crtl : 3; + bool tmp_ext : 1; + bool prs_ready : 1; + bool tmp_ready : 1; + bool sensor_ready : 1; + bool coef_ready : 1; + } bit; + uint8_t reg; + } meas_ = {.reg = 0}; // MEAS_REG + + union { + struct { + uint8_t _reserved : 5; + bool int_prs : 1; + bool int_tmp : 1; + bool int_fifo_full : 1; + } bit; + uint8_t reg; + } int_status_ = {.reg = 0}; // INT_STS + + union { + struct { + bool spi_3wire : 1; + bool fifo_en : 1; + bool p_shift : 1; + bool t_shift : 1; + bool int_prs : 1; + bool int_tmp : 1; + bool int_fifo : 1; + bool int_hl : 1; + } bit; + uint8_t reg; + } cfg_ = {.reg = 0}; // CFG_REG + + union { + struct { + bool fifo_empty : 1; + bool fifo_full : 1; + uint8_t _reserved : 6; + } bit; + uint8_t reg; + } fifo_sts_ = {.reg = 0}; // FIFO_STS + + union { + struct { + // Set to true to flush FIFO + bool fifo_flush : 1; + // Reserved bits + uint8_t _reserved : 3; + // Soft reset. Set to 1001 (0x9) to perform reset. + uint8_t soft_rst : 4; + } bit; + uint8_t reg = 0; + } reset_ = {.reg = 0}; // RESET + + union { + struct { + uint8_t prod_id : 4; + uint8_t rev_id : 4; + } bit; + uint8_t reg = 0; + } prod_id_ = {.reg = 0}; // ID + +}; // class SPA06Component +} // namespace esphome::spa06_base diff --git a/esphome/components/spa06_i2c/__init__.py b/esphome/components/spa06_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/spa06_i2c/sensor.py b/esphome/components/spa06_i2c/sensor.py new file mode 100644 index 0000000000..b48a5bca50 --- /dev/null +++ b/esphome/components/spa06_i2c/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv + +from ..spa06_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["spa06_base"] +CODEOWNERS = ["@danielkent-net"] +DEPENDENCIES = ["i2c"] + +spa06_ns = cg.esphome_ns.namespace("spa06_i2c") +SPA06I2CComponent = spa06_ns.class_( + "SPA06I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(SPA06I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/spa06_i2c/spa06_i2c.cpp b/esphome/components/spa06_i2c/spa06_i2c.cpp new file mode 100644 index 0000000000..4970b0822d --- /dev/null +++ b/esphome/components/spa06_i2c/spa06_i2c.cpp @@ -0,0 +1,14 @@ +#include "spa06_i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::spa06_i2c { + +static const char *const TAG = "spa06_i2c"; + +void SPA06I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + SPA06Component::dump_config(); +} + +} // namespace esphome::spa06_i2c diff --git a/esphome/components/spa06_i2c/spa06_i2c.h b/esphome/components/spa06_i2c/spa06_i2c.h new file mode 100644 index 0000000000..6b4bce3a4e --- /dev/null +++ b/esphome/components/spa06_i2c/spa06_i2c.h @@ -0,0 +1,20 @@ +#pragma once +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::spa06_i2c { + +class SPA06I2CComponent : public spa06_base::SPA06Component, public i2c::I2CDevice { + public: + bool spa_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } + bool spa_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } + bool spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return read_bytes(a_register, data, len); + } + bool spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return write_bytes(a_register, data, len); + } + void dump_config() override; +}; + +} // namespace esphome::spa06_i2c diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index 9d02ac9c74..2caab828fb 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -457,6 +457,9 @@ void SpeakerSourceMediaPlayer::process_control_queue_() { if (ps.playlist_index < ps.playlist.size()) { this->queue_play_current_(pipeline, ps.playlist_delay_ms); } else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) { + if (!ps.shuffle_indices.empty()) { + this->shuffle_playlist_(pipeline); + } ps.playlist_index = 0; this->queue_play_current_(pipeline, ps.playlist_delay_ms); } diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index d1f7452054..0802cdec8e 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -335,8 +335,8 @@ Sprinkler::Sprinkler(const char *name) : name_(name) { // The `name` is stored for dump_config logging this->timer_.init(2); // Timer names only need to be unique within this component instance - this->timer_.push_back({"sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); - this->timer_.push_back({"vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); + this->timer_.push_back({"sm", false, 0, 0, [this]() { this->sm_timer_callback_(); }}); + this->timer_.push_back({"vs", false, 0, 0, [this]() { this->valve_selection_callback_(); }}); } void Sprinkler::setup() { diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 651aa3c489..a224ab8459 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -16,17 +16,15 @@ static const char *const TAG = "template.alarm_control_panel"; TemplateAlarmControlPanel::TemplateAlarmControlPanel(){}; #ifdef USE_BINARY_SENSOR -void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags, AlarmSensorType type) { - // Save the flags and type. Assign a store index for the per sensor data type. - SensorDataStore sd; - sd.last_chime_state = false; +void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint8_t flags, AlarmSensorType type) { + // Save the sensor pointer, flags, and type in the per-sensor info structure. AlarmSensor alarm_sensor; alarm_sensor.sensor = sensor; alarm_sensor.info.flags = flags; alarm_sensor.info.type = type; - alarm_sensor.info.store_index = this->next_store_index_++; + alarm_sensor.info.chime_active = false; + alarm_sensor.info.auto_bypassed = false; this->sensors_.push_back(alarm_sensor); - this->sensor_data_.push_back(sd); }; // Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS @@ -55,7 +53,7 @@ void TemplateAlarmControlPanel::dump_config() { (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR for (const auto &alarm_sensor : this->sensors_) { - const uint16_t flags = alarm_sensor.info.flags; + const uint8_t flags = alarm_sensor.info.flags; ESP_LOGCONFIG(TAG, " Binary Sensor:\n" " Name: %s\n" @@ -95,7 +93,7 @@ void TemplateAlarmControlPanel::loop() { delay = this->arming_night_time_; } if ((millis() - this->last_update_) > delay) { - this->bypass_before_arming(); + this->auto_bypass_sensors_(); this->publish_state(this->desired_state_); } return; @@ -117,26 +115,25 @@ void TemplateAlarmControlPanel::loop() { #ifdef USE_BINARY_SENSOR // Test all of the sensors regardless of the alarm panel state - for (const auto &alarm_sensor : this->sensors_) { - const auto &info = alarm_sensor.info; + for (auto &alarm_sensor : this->sensors_) { + auto &info = alarm_sensor.info; auto *sensor = alarm_sensor.sensor; // Check for chime zones if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open - if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) { + if ((!info.chime_active) && (sensor->state)) { // Must be disarmed to chime if (this->current_state_ == ACP_STATE_DISARMED) { this->chime_callback_.call(); } } // Record the sensor state change - this->sensor_data_[info.store_index].last_chime_state = sensor->state; + info.chime_active = sensor->state; } // Check for faulted sensors if (sensor->state) { // Skip if auto bypassed - if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(), - info.store_index) == 1) { + if (info.auto_bypassed) { continue; } // Skip if bypass armed home @@ -239,23 +236,33 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p if (delay > 0) { this->publish_state(ACP_STATE_ARMING); } else { - this->bypass_before_arming(); + this->auto_bypass_sensors_(); this->publish_state(state); } } -void TemplateAlarmControlPanel::bypass_before_arming() { +void TemplateAlarmControlPanel::auto_bypass_sensors_() { #ifdef USE_BINARY_SENSOR - for (const auto &alarm_sensor : this->sensors_) { + for (auto &alarm_sensor : this->sensors_) { + auto &info = alarm_sensor.info; + auto *sensor = alarm_sensor.sensor; // Check for faulted bypass_auto sensors and remove them from monitoring - if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) { - ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index); + if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); + info.auto_bypassed = true; } } #endif } +void TemplateAlarmControlPanel::clear_auto_bypassed_sensors_() { +#ifdef USE_BINARY_SENSOR + for (auto &alarm_sensor : this->sensors_) { + alarm_sensor.info.auto_bypassed = false; + } +#endif +} + void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { auto opt_state = call.get_state(); if (opt_state) { @@ -273,9 +280,7 @@ void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { } this->desired_state_ = ACP_STATE_DISARMED; this->publish_state(ACP_STATE_DISARMED); -#ifdef USE_BINARY_SENSOR - this->bypassed_sensor_indicies_.clear(); -#endif + this->clear_auto_bypassed_sensors_(); } else if (state == ACP_STATE_TRIGGERED) { this->publish_state(ACP_STATE_TRIGGERED); } else if (state == ACP_STATE_PENDING) { diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 4f32e99fd7..57a99f2830 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -18,7 +18,7 @@ namespace esphome::template_ { #ifdef USE_BINARY_SENSOR -enum BinarySensorFlags : uint16_t { +enum BinarySensorFlags : uint8_t { BINARY_SENSOR_MODE_NORMAL = 1 << 0, BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1, BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT = 1 << 2, @@ -41,14 +41,11 @@ enum TemplateAlarmControlPanelRestoreMode { }; #ifdef USE_BINARY_SENSOR -struct SensorDataStore { - bool last_chime_state; -}; - struct SensorInfo { - uint16_t flags; + uint8_t flags; AlarmSensorType type; - uint8_t store_index; + bool chime_active; + bool auto_bypassed; }; struct AlarmSensor { @@ -68,7 +65,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } - void bypass_before_arming(); + // Remove before 2026.10.0 + ESPDEPRECATED("bypass_before_arming() is deprecated and will be removed in 2026.10.0", "2026.4.0") + void bypass_before_arming() { this->auto_bypass_sensors_(); } #ifdef USE_BINARY_SENSOR /** Initialize the sensors vector with the specified capacity. @@ -83,7 +82,7 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl * @param flags The OR of BinarySensorFlags for the sensor. * @param type The sensor type which determines its triggering behaviour. */ - void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0, + void add_sensor(binary_sensor::BinarySensor *sensor, uint8_t flags = 0, AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); #endif @@ -141,11 +140,6 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl #ifdef USE_BINARY_SENSOR // List of binary sensors with their alarm-specific info FixedVector sensors_; - // a list of automatically bypassed sensors - std::vector bypassed_sensor_indicies_; - // Per sensor data store - std::vector sensor_data_; - uint8_t next_store_index_ = 0; #endif TemplateAlarmControlPanelRestoreMode restore_mode_{}; @@ -170,6 +164,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl bool is_code_valid_(optional code); void arm_(optional code, alarm_control_panel::AlarmControlPanelState state, uint32_t delay); + void auto_bypass_sensors_(); + void clear_auto_bypassed_sensors_(); }; } // namespace esphome::template_ diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 7ffa408db9..9821046a73 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -59,15 +59,20 @@ _DST_RULE_TYPE_MAP = { def _load_tzdata(iana_key: str) -> bytes | None: # From https://tzdata.readthedocs.io/en/latest/#examples + if not iana_key: + return None try: package_loc, resource = iana_key.rsplit("/", 1) except ValueError: - return None - package = "tzdata.zoneinfo." + package_loc.replace("/", ".") + # Handle top-level timezone entries like "UTC", "GMT" + package = "tzdata.zoneinfo" + resource = iana_key + else: + package = "tzdata.zoneinfo." + package_loc.replace("/", ".") try: return (resources.files(package) / resource).read_bytes() - except (FileNotFoundError, ModuleNotFoundError): + except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError): return None diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 2c4fb34c9a..899d349e21 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,7 +45,7 @@ class UARTDevice { size_t available() { return this->parent_->available(); } - FlushResult flush() { return this->parent_->flush(); } + UARTFlushResult flush() { return this->parent_->flush(); } // Compat APIs int read() { diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index ee2b006039..abc77fbae8 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -30,11 +30,11 @@ enum UARTDirection { const LogString *parity_to_str(UARTParityOptions parity); /// Result of a flush() call. -enum class FlushResult { - SUCCESS, ///< Confirmed: all bytes left the TX FIFO. - TIMEOUT, ///< Confirmed: timed out before TX completed. - FAILED, ///< Confirmed: driver or hardware error. - ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. +enum class UARTFlushResult { + UART_FLUSH_RESULT_SUCCESS, ///< Confirmed: all bytes left the TX FIFO. + UART_FLUSH_RESULT_TIMEOUT, ///< Confirmed: timed out before TX completed. + UART_FLUSH_RESULT_FAILED, ///< Confirmed: driver or hardware error. + UART_FLUSH_RESULT_ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed. }; class UARTComponent { @@ -82,8 +82,8 @@ class UARTComponent { virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. - // @return FlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. - virtual FlushResult flush() = 0; + // @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. + virtual UARTFlushResult flush() = 0; // Sets the maximum time to wait for TX to drain during flush(). // Only meaningful on ESP32 (IDF). Other platforms ignore this value. diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 91218c4300..0ea7930760 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -213,14 +213,14 @@ size_t ESP8266UartComponent::available() { return this->sw_serial_->available(); } } -FlushResult ESP8266UartComponent::flush() { +UARTFlushResult ESP8266UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); if (this->hw_serial_ != nullptr) { this->hw_serial_->flush(); } else { this->sw_serial_->flush(); } - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index ca90dc5964..7f844d9b65 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -58,7 +58,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 47ddf1a38d..bd2f915d3a 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -7,7 +7,9 @@ #include "esphome/core/log.h" #include "esphome/core/gpio.h" #include "driver/gpio.h" +#include "esp_private/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_UART_WAKE_LOOP_ON_RX #include "esphome/core/application.h" @@ -21,6 +23,20 @@ namespace esphome::uart { static const char *const TAG = "uart.idf"; +/// Check if a pin number matches one of the default UART0 GPIO pins. +/// These pins may have residual IOMUX state from the ROM bootloader that +/// must be cleared before UART reconfiguration. +/// +/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix, +/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only +/// calls gpio_input_enable() which does NOT clear the IOMUX function select. +/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later +/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active, +/// causing TX data to loop back into RX on the same pin. +static constexpr bool is_default_uart0_pin(int8_t pin_num) { + return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM; +} + uart_config_t IDFUARTComponent::get_config_() { uart_parity_t parity = UART_PARITY_DISABLE; if (this->parity_ == UART_CONFIG_PARITY_EVEN) { @@ -131,6 +147,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Clear residual IOMUX function on UART0 default pins left by the ROM bootloader. + // See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug. + if (is_default_uart0_pin(tx)) { + gpio_func_sel(static_cast(tx), PIN_FUNC_GPIO); + } + if (is_default_uart0_pin(rx)) { + gpio_func_sel(static_cast(rx), PIN_FUNC_GPIO); + } + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -146,10 +175,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; @@ -335,15 +360,15 @@ size_t IDFUARTComponent::available() { return available; } -FlushResult IDFUARTComponent::flush() { +UARTFlushResult IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_); esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks); if (err == ESP_OK) - return FlushResult::SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; if (err == ESP_ERR_TIMEOUT) - return FlushResult::TIMEOUT; - return FlushResult::FAILED; + return UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return UARTFlushResult::UART_FLUSH_RESULT_FAILED; } void IDFUARTComponent::check_logger_conflict() {} diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 9fa2013cfd..ec4f2884b2 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -31,7 +31,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; } diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 66026f3ccd..0042ffae23 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -274,13 +274,13 @@ size_t HostUartComponent::available() { return result; }; -FlushResult HostUartComponent::flush() { +UARTFlushResult HostUartComponent::flush() { if (this->file_descriptor_ == -1) { - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } tcflush(this->file_descriptor_, TCIOFLUSH); ESP_LOGV(TAG, " Flushing"); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void HostUartComponent::update_error_(const std::string &error) { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index c22efdcb92..56ff525bc3 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -18,7 +18,7 @@ class HostUartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; protected: diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 6a550f296a..4172e7c164 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -170,10 +170,10 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { } size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } -FlushResult LibreTinyUARTComponent::flush() { +UARTFlushResult LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void LibreTinyUARTComponent::check_logger_conflict() { diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 77df808067..872ea86601 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -22,7 +22,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 6f6f1fb96b..1aaf98dc84 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -208,10 +208,10 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { return true; } size_t RP2040UartComponent::available() { return this->serial_->available(); } -FlushResult RP2040UartComponent::flush() { +UARTFlushResult RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); - return FlushResult::ASSUMED_SUCCESS; + return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index 891212ca74..198c698af9 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -25,7 +25,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool read_array(uint8_t *data, size_t len) override; size_t available() override; - FlushResult flush() override; + UARTFlushResult flush() override; uint16_t get_config(); diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index d3f7e69444..a354b198b4 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,11 +6,17 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; +static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us of each other (noise filtering) +static constexpr uint32_t START_DELAY_US = 100; // Ignore edges within 100us of trigger (filters bleed-through) static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (arg->echo_pin_isr.digital_read()) { + // Ignore edges after measurement complete or too soon after trigger pulse + if (arg->echo_end || (now - arg->measurement_start_us) <= START_DELAY_US) { + return; + } + if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -21,15 +27,14 @@ void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() { InterruptLock lock; - this->store_.echo_start_us = 0; - this->store_.echo_end_us = 0; this->store_.echo_start = false; this->store_.echo_end = false; + this->store_.measurement_start_us = micros(); this->trigger_pin_isr_.digital_write(true); delayMicroseconds(this->pulse_time_us_); this->trigger_pin_isr_.digital_write(false); this->measurement_pending_ = true; - this->measurement_start_us_ = micros(); + this->measurement_start_us_ = this->store_.measurement_start_us; } void UltrasonicSensorComponent::setup() { @@ -37,7 +42,6 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); - this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -77,17 +81,10 @@ void UltrasonicSensorComponent::loop() { } if (this->store_.echo_end) { - float result; - if (this->store_.echo_start) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", - this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); - result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); - } else { - ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); - result = NAN; - } + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); + float result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); this->publish_state(result); this->measurement_pending_ = false; return; diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 541f7d2b70..7d333a1b24 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,8 +11,7 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); - ISRInternalGPIOPin echo_pin_isr; - volatile uint32_t wait_start_us{0}; + volatile uint32_t measurement_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index a56abc9ee8..89405ab893 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -82,7 +82,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parentedhas_peek_ ? 1 : 0); } -uart::FlushResult USBCDCACMInstance::flush() { +uart::UARTFlushResult USBCDCACMInstance::flush() { // Wait for TX ring buffer to be empty if (this->usb_tx_ringbuf_ == nullptr) { - return uart::FlushResult::ASSUMED_SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } UBaseType_t waiting = 1; @@ -342,10 +342,10 @@ uart::FlushResult USBCDCACMInstance::flush() { // Also wait for USB to finish transmitting esp_err_t err = tinyusb_cdcacm_write_flush(static_cast(this->itf_), pdMS_TO_TICKS(100)); if (err == ESP_OK) - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; if (err == ESP_ERR_TIMEOUT) - return uart::FlushResult::TIMEOUT; - return uart::FlushResult::FAILED; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED; } void USBCDCACMInstance::check_logger_conflict() {} diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index a5d312f191..0b8589f671 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -169,7 +169,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) { this->parent_->start_output(this); } -uart::FlushResult USBUartChannel::flush() { +uart::UARTFlushResult USBUartChannel::flush() { // Spin until the output queue is drained and the last USB transfer completes. // Safe to call from the main loop only. // The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush; @@ -181,8 +181,8 @@ uart::FlushResult USBUartChannel::flush() { yield(); } if (!this->output_queue_.empty() || this->output_started_.load()) - return uart::FlushResult::TIMEOUT; - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } bool USBUartChannel::peek_byte(uint8_t *data) { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 7a06b04f11..8a47f0cf4b 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -140,7 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parentedinput_buffer_.get_available(); } - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } void set_debug(bool debug) { this->debug_ = debug; } diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index 87e9cde088..a064f375f7 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -87,24 +87,30 @@ template class ValveIsClosedCondition : public Condition class ValveOpenTrigger : public Trigger<> { public: - ValveOpenTrigger(Valve *a_valve) { - a_valve->add_on_state_callback([this, a_valve]() { - if (a_valve->is_fully_open()) { + ValveOpenTrigger(Valve *a_valve) : valve_(a_valve) { + a_valve->add_on_state_callback([this]() { + if (this->valve_->is_fully_open()) { this->trigger(); } }); } + + protected: + Valve *valve_; }; class ValveClosedTrigger : public Trigger<> { public: - ValveClosedTrigger(Valve *a_valve) { - a_valve->add_on_state_callback([this, a_valve]() { - if (a_valve->is_fully_closed()) { + ValveClosedTrigger(Valve *a_valve) : valve_(a_valve) { + a_valve->add_on_state_callback([this]() { + if (this->valve_->is_fully_closed()) { this->trigger(); } }); } + + protected: + Valve *valve_; }; } // namespace valve diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 60816fc6dd..38ccfccb76 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -121,7 +121,10 @@ void AsyncWebServer::begin() { if (this->server_) { this->end(); } + // Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's + // 640-byte stack buffer used by web_server JSON request handlers. httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = config.stack_size + 256; config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Always enable LRU purging to handle socket exhaustion gracefully. @@ -259,19 +262,6 @@ StringRef AsyncWebServerRequest::url_to(std::span buffer) co return StringRef(buffer.data(), decoded_len); } -void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { - httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); -} - -void AsyncWebServerRequest::send(int code, const char *content_type, const char *content) { - this->init_response_(nullptr, code, content_type); - if (content) { - httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); - } else { - httpd_resp_send(*this, nullptr, 0); - } -} - void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 9d81d89ec7..81683e8d85 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -134,8 +134,17 @@ class AsyncWebServerRequest { void redirect(const std::string &url); - void send(AsyncWebServerResponse *response); - void send(int code, const char *content_type = nullptr, const char *content = nullptr); + inline void ESPHOME_ALWAYS_INLINE send(AsyncWebServerResponse *response) { + httpd_resp_send(*this, response->get_content_data(), response->get_content_size()); + } + inline void ESPHOME_ALWAYS_INLINE send(int code, const char *content_type = nullptr, const char *content = nullptr) { + this->init_response_(nullptr, code, content_type); + if (content) { + httpd_resp_send(*this, content, HTTPD_RESP_USE_STRLEN); + } else { + httpd_resp_send(*this, nullptr, 0); + } + } // NOLINTNEXTLINE(readability-identifier-naming) AsyncWebServerResponse *beginResponse(int code, const char *content_type) { auto *res = new AsyncWebServerResponseEmpty(this); // NOLINT(cppcoreguidelines-owning-memory) diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index f01d164e9f..2ec5632691 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -433,16 +433,16 @@ void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) { this->reg(0).write_fifo(const_cast(buffer), length); } -uart::FlushResult WeikaiChannel::flush() { +uart::UARTFlushResult WeikaiChannel::flush() { uint32_t const start_time = millis(); while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty if (millis() - start_time > 200) { ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_()); - return uart::FlushResult::TIMEOUT; + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; } yield(); // reschedule our thread to avoid blocking } - return uart::FlushResult::SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } size_t WeikaiChannel::xfer_fifo_to_buffer_() { diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 715b82bfc7..36d8f66265 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -380,7 +380,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data /// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore /// we wait until all bytes are gone with a timeout of 100 ms - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; protected: friend class WeikaiComponent; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 9f73b1cc6f..33557f03c7 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -235,6 +235,14 @@ def validate_variant(_): variant = get_esp32_variant() if variant in NO_WIFI_VARIANTS and "esp32_hosted" not in fv.full_config.get(): raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") + if CORE.is_rp2040: + from esphome.components.rp2040 import board_has_wifi, get_board + + if not board_has_wifi(): + raise cv.Invalid( + f"Board '{get_board()}' does not have WiFi support (no CYW43 wireless chip). " + f"Use a WiFi-capable board like 'rpipicow' or 'rpipico2w'." + ) def _apply_min_auth_mode_default(config): diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 346276692a..0fd1385258 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -871,9 +871,6 @@ void WiFiComponent::loop() { WiFiComponent::WiFiComponent() { global_wifi_component = this; } -bool WiFiComponent::has_ap() const { return this->has_ap_; } -bool WiFiComponent::is_ap_active() const { return this->ap_started_; } -bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } #ifdef USE_WIFI_11KV_SUPPORT void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } @@ -894,10 +891,6 @@ network::IPAddress WiFiComponent::get_dns_address(int num) { return this->wifi_dns_ip_(num); return {}; } -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *WiFiComponent::get_use_address() const { return this->use_address_; } -void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } #ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { @@ -2250,8 +2243,6 @@ bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; } #ifdef USE_WIFI_WPA2_EAP const optional &WiFiAP::get_eap() const { return this->eap_; } #endif -uint8_t WiFiAP::get_channel() const { return this->channel_; } -bool WiFiAP::has_channel() const { return this->channel_ != 0; } #ifdef USE_WIFI_MANUAL_IP const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 057f2c0661..718f4a6e12 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -263,8 +263,8 @@ class WiFiAP { #ifdef USE_WIFI_WPA2_EAP const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP - uint8_t get_channel() const; - bool has_channel() const; + uint8_t get_channel() const { return this->channel_; } + bool has_channel() const { return this->channel_ != 0; } int8_t get_priority() const { return priority_; } #ifdef USE_WIFI_MANUAL_IP const optional &get_manual_ip() const; @@ -470,9 +470,9 @@ class WiFiComponent final : public Component { /// Reconnect WiFi if required. void loop() override; - bool has_sta() const; - bool has_ap() const; - bool is_ap_active() const; + bool has_sta() const { return !this->sta_.empty(); } + bool has_ap() const { return this->has_ap_; } + bool is_ap_active() const { return this->ap_started_; } #ifdef USE_WIFI_11KV_SUPPORT void set_btm(bool btm); @@ -481,8 +481,8 @@ class WiFiComponent final : public Component { network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); - const char *get_use_address() const; - void set_use_address(const char *use_address); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0bf7934878..f2fabb9080 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -92,13 +92,23 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return ret; } bool WiFiComponent::wifi_apply_power_save_() { + // ESP8266 sleep types have confusing names — LIGHT_SLEEP_T is the MORE aggressive mode. + // SDK enum: NONE_SLEEP_T=0, LIGHT_SLEEP_T=1, MODEM_SLEEP_T=2 + // https://github.com/esp8266/Arduino/blob/3.1.2/tools/sdk/include/user_interface.h#L447-L451 + // Arduino ESP32 compat confirms: WIFI_PS_MIN_MODEM=MODEM_SLEEP, WIFI_PS_MAX_MODEM=LIGHT_SLEEP + // https://github.com/esp8266/Arduino/blob/3.1.2/libraries/ESP8266WiFi/src/ESP8266WiFiType.h#L53-L55 sleep_type_t power_save; switch (this->power_save_) { case WIFI_POWER_SAVE_LIGHT: - power_save = LIGHT_SLEEP_T; + // MODEM_SLEEP_T: only the WiFi modem sleeps between DTIM beacons, CPU stays active. + // Matches ESP32's WIFI_PS_MIN_MODEM. + power_save = MODEM_SLEEP_T; break; case WIFI_POWER_SAVE_HIGH: - power_save = MODEM_SLEEP_T; + // LIGHT_SLEEP_T: both WiFi modem AND CPU suspend between DTIM beacons. + // Most aggressive — prevents TCP processing during sleep. Matches ESP32's WIFI_PS_MAX_MODEM. + // See https://github.com/esphome/esphome/issues/14999 + power_save = LIGHT_SLEEP_T; break; case WIFI_POWER_SAVE_NONE: default: @@ -725,7 +735,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } } ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(), - needs_full ? "" : " (filtered)"); + needs_full ? LOG_STR_LITERAL("") : LOG_STR_LITERAL(" (filtered)")); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS this->pending_.scan_complete = true; // Defer listener callbacks to main loop diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 4c4150e44d..b049a0413c 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -629,8 +629,8 @@ void WiFiComponent::wifi_pre_setup_() { return; } - auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); - WiFi.onEvent(f); + WiFi.onEvent( + [this](arduino_event_id_t event, arduino_event_info_t info) { this->wifi_event_callback_(event, info); }); // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); } diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 2022e25b6c..b4641894db 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -2,7 +2,6 @@ #ifdef USE_WIREGUARD #include #include -#include #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -48,8 +47,8 @@ void Wireguard::setup() { if (this->wg_initialized_ == ESP_OK) { ESP_LOGI(TAG, "Initialized"); this->wg_peer_offline_time_ = millis(); - this->srctime_->add_on_time_sync_callback(std::bind(&Wireguard::start_connection_, this)); - this->defer(std::bind(&Wireguard::start_connection_, this)); // defer to avoid blocking setup + this->srctime_->add_on_time_sync_callback([this]() { this->start_connection_(); }); + this->defer([this]() { this->start_connection_(); }); // defer to avoid blocking setup #ifdef USE_TEXT_SENSOR if (this->address_sensor_ != nullptr) { @@ -206,7 +205,7 @@ void Wireguard::enable() { void Wireguard::disable() { this->enabled_ = false; - this->defer(std::bind(&Wireguard::stop_connection_, this)); // defer to avoid blocking running loop + this->defer([this]() { this->stop_connection_(); }); // defer to avoid blocking running loop ESP_LOGI(TAG, "Disabled"); this->publish_enabled_state(); } diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp index ee3ad316e1..0f27f09c87 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -58,15 +58,13 @@ bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) #ifdef USE_BINARY_SENSOR if (res->has_motion.has_value() && this->motion_ != nullptr) { this->motion_->publish_state(*res->has_motion); - this->set_timeout("motion_timeout", this->motion_timeout_, - [this, res]() { this->motion_->publish_state(false); }); + this->set_timeout("motion_timeout", this->motion_timeout_, [this]() { this->motion_->publish_state(false); }); } if (res->is_light.has_value() && this->light_ != nullptr) this->light_->publish_state(*res->is_light); if (res->button_press.has_value() && this->button_ != nullptr) { this->button_->publish_state(*res->button_press); - this->set_timeout("button_timeout", this->button_timeout_, - [this, res]() { this->button_->publish_state(false); }); + this->set_timeout("button_timeout", this->button_timeout_, [this]() { this->button_->publish_state(false); }); } #endif #ifdef USE_SENSOR diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index b8a091feb9..348e7a3cf2 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -122,6 +122,8 @@ def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("FPU", True) zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) zephyr_add_prj_conf("STD_CPP20", True) + # random_bytes() uses sys_rand_get() which requires the entropy subsystem + zephyr_add_prj_conf("ENTROPY_GENERATOR", True) # os: ***** USAGE FAULT ***** # os: Illegal load of EXC_RETURN into PC diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7c77fdd2c..a3b0471ebc 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -75,7 +75,6 @@ IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } // Zephyr LwIPLock is defined inline as a no-op in helpers.h -uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) bool random_bytes(uint8_t *data, size_t len) { sys_rand_get(data, len); return true; diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index df69c0e652..c26a1d6d53 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -10,7 +10,7 @@ namespace esphome::zephyr { -static const char *const TAG = "zephyr.preferences"; +static const char *const TAG = "preferences"; bool ZephyrPreferenceBackend::save(const uint8_t *data, size_t len) { this->data.resize(len); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 32689dab27..56f255a076 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -494,6 +494,13 @@ def hex_int(value): return HexInt(int_(value)) +def int_to_hex_string(value: int | str) -> str: + """Convert an integer to a hex string (e.g. 64 -> '0x40'). Pass-through strings.""" + if isinstance(value, int): + return f"0x{value:X}" + return value + + def int_(value): """Validate that the config option is an integer. diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3a9e825e04..c020a8ed58 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -12,21 +12,11 @@ #endif #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" -#ifdef USE_ESP32 -#include -#include -#else -#include -#include -#endif #endif // USE_LWIP_FAST_SELECT #include "esphome/core/version.h" #include "esphome/core/hal.h" #include #include -#ifdef USE_RUNTIME_STATS -#include "esphome/components/runtime_stats/runtime_stats.h" -#endif #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -163,66 +153,6 @@ void Application::setup() { this->schedule_dump_config(); } -void Application::loop() { - uint8_t new_app_state = 0; - - // Get the initial loop time at the start - uint32_t last_op_end_time = millis(); - - this->before_loop_tasks_(last_op_end_time); - - for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; - this->current_loop_index_++) { - Component *component = this->looping_components_[this->current_loop_index_]; - - // Update the cached time before each component runs - this->loop_component_start_time_ = last_op_end_time; - - { - this->set_current_component(component); - WarnIfComponentBlockingGuard guard{component, last_op_end_time}; - component->loop(); - // Use the finish method to get the current time as the end time - last_op_end_time = guard.finish(); - } - new_app_state |= component->get_component_state(); - this->app_state_ |= new_app_state; - this->feed_wdt(last_op_end_time); - } - - this->after_loop_tasks_(); - this->app_state_ = new_app_state; - -#ifdef USE_RUNTIME_STATS - // Process any pending runtime stats printing after all components have run - // This ensures stats printing doesn't affect component timing measurements - if (global_runtime_stats != nullptr) { - global_runtime_stats->process_pending_stats(last_op_end_time); - } -#endif - - // Use the last component's end time instead of calling millis() again - auto elapsed = last_op_end_time - this->last_loop_; - if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { - // Even if we overran the loop interval, we still need to select() - // to know if any sockets have data ready - this->yield_with_select_(0); - } else { - uint32_t delay_time = this->loop_interval_ - elapsed; - uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); - // next_schedule is max 0.5*delay_time - // otherwise interval=0 schedules result in constant looping with almost no sleep - next_schedule = std::max(next_schedule, delay_time / 2); - delay_time = std::min(next_schedule, delay_time); - - this->yield_with_select_(delay_time); - } - this->last_loop_ = last_op_end_time; - - if (this->dump_config_at_ < this->components_.size()) { - this->process_dump_config_(); - } -} void Application::process_dump_config_() { if (this->dump_config_at_ == 0) { @@ -394,22 +324,6 @@ void Application::teardown_components(uint32_t timeout_ms) { } } -void Application::calculate_looping_components_() { - // FixedVector capacity was pre-initialized by codegen with the exact count - // of components that override loop(), computed at C++ compile time. - - // Add all components with loop override that aren't already LOOP_DONE - // Some components (like logger) may call disable_loop() during initialization - // before setup runs, so we need to respect their LOOP_DONE state - this->add_looping_components_by_state_(false); - - this->looping_components_active_end_ = this->looping_components_.size(); - - // Then add any components that are already LOOP_DONE to the inactive section - // This handles components that called disable_loop() during initialization - this->add_looping_components_by_state_(true); -} - void Application::add_looping_components_by_state_(bool match_loop_done) { for (auto *obj : this->components_) { if (obj->has_overridden_loop() && @@ -525,41 +439,6 @@ void Application::enable_pending_loops_() { } } -void Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) - // Drain wake notifications first to clear socket for next wake - this->drain_wake_notifications_(); -#endif - - // Process scheduled tasks - this->scheduler.call(loop_start_time); - - // Feed the watchdog timer - this->feed_wdt(loop_start_time); - - // Process any pending enable_loop requests from ISRs - // This must be done before marking in_loop_ = true to avoid race conditions - if (this->has_pending_enable_loop_requests_) { - // Clear flag BEFORE processing to avoid race condition - // If ISR sets it during processing, we'll catch it next loop iteration - // This is safe because: - // 1. Each component has its own pending_enable_loop_ flag that we check - // 2. If we can't process a component (wrong state), enable_pending_loops_() - // will set this flag back to true - // 3. Any new ISR requests during processing will set the flag again - this->has_pending_enable_loop_requests_ = false; - this->enable_pending_loops_(); - } - - // Mark that we're in the loop for safe reentrant modifications - this->in_loop_ = true; -} - -void Application::after_loop_tasks_() { - // Clear the in_loop_ flag to indicate we're done processing components - this->in_loop_ = false; -} - #ifdef USE_LWIP_FAST_SELECT bool Application::register_socket(struct lwip_sock *sock) { // It modifies monitored_sockets_ without locking — must only be called from the main loop. @@ -641,36 +520,10 @@ void Application::unregister_socket_fd(int fd) { #endif +// Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) void Application::yield_with_select_(uint32_t delay_ms) { - // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. - // Safe because this runs on the main loop which owns socket lifetime (create, read, close). - if (delay_ms == 0) [[unlikely]] { - yield(); - return; - } - - // Check if any socket already has pending data before sleeping. - // If a socket still has unread data (rcvevent > 0) but the task notification was already - // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. - // This scan preserves select() semantics: return immediately when any fd is ready. - for (struct lwip_sock *sock : this->monitored_sockets_) { - if (esphome_lwip_socket_has_data(sock)) { - yield(); - return; - } - } - - // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. - // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — - // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); - -#elif defined(USE_SOCKET_SELECT_SUPPORT) // Fallback select() path (host platform and any future platforms without fast select). - // ESP32 and LibreTiny are excluded by the #if above — they use the fast path. if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed if (this->socket_fds_changed_) [[unlikely]] { @@ -717,16 +570,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity - // ESP8266: via esp_schedule() - // RP2040: via __sev()/__wfe() hardware sleep/wake - socket::socket_delay(delay_ms); -#else - // No select support, use regular delay - delay(delay_ms); -#endif } +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. diff --git a/esphome/core/application.h b/esphome/core/application.h index 23bb209eaf..06ff30e81f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -27,6 +27,13 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#ifdef USE_ESP32 +#include +#include +#else +#include +#include +#endif #else #include #ifdef USE_WAKE_LOOP_THREADSAFE @@ -34,9 +41,13 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT +#ifdef USE_RUNTIME_STATS +#include "esphome/components/runtime_stats/runtime_stats.h" +#endif #if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { -void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket #endif #ifdef USE_BINARY_SENSOR @@ -293,7 +304,7 @@ class Application { void setup(); /// Make a loop iteration. Call this in your loop() function. - void loop(); + inline void ESPHOME_ALWAYS_INLINE loop(); /// Get the name of this Application set by pre_setup(). const StringRef &get_name() const { return this->name_; } @@ -595,7 +606,19 @@ class Application { void register_component_impl_(Component *comp, bool has_loop); - void calculate_looping_components_(); + void calculate_looping_components_() { + // FixedVector capacity was pre-initialized by codegen with the exact count + // of components that override loop(), computed at C++ compile time. + + // Add all components with loop override that aren't already LOOP_DONE + // Some components (like logger) may call disable_loop() during initialization + // before setup runs, so we need to respect their LOOP_DONE state + this->add_looping_components_by_state_(false); + this->looping_components_active_end_ = this->looping_components_.size(); + // Then add any components that are already LOOP_DONE to the inactive section + // This handles components that called disable_loop() during initialization + this->add_looping_components_by_state_(true); + } void add_looping_components_by_state_(bool match_loop_done); // These methods are called by Component::disable_loop() and Component::enable_loop() @@ -605,8 +628,8 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); - void before_loop_tasks_(uint32_t loop_start_time); - void after_loop_tasks_(); + inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time); + inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; } /// Process dump_config output one component per loop iteration. /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. @@ -616,7 +639,12 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) + // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); +#else + inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) void setup_wake_loop_threadsafe_(); // Create wake notification socket @@ -802,4 +830,134 @@ inline void Application::drain_wake_notifications_() { } #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + + // Process scheduled tasks + this->scheduler.call(loop_start_time); + + // Feed the watchdog timer + this->feed_wdt(loop_start_time); + + // Process any pending enable_loop requests from ISRs + // This must be done before marking in_loop_ = true to avoid race conditions + if (this->has_pending_enable_loop_requests_) { + // Clear flag BEFORE processing to avoid race condition + // If ISR sets it during processing, we'll catch it next loop iteration + // This is safe because: + // 1. Each component has its own pending_enable_loop_ flag that we check + // 2. If we can't process a component (wrong state), enable_pending_loops_() + // will set this flag back to true + // 3. Any new ISR requests during processing will set the flag again + this->has_pending_enable_loop_requests_ = false; + this->enable_pending_loops_(); + } + + // Mark that we're in the loop for safe reentrant modifications + this->in_loop_ = true; +} + +inline void ESPHOME_ALWAYS_INLINE Application::loop() { + uint8_t new_app_state = 0; + + // Get the initial loop time at the start + uint32_t last_op_end_time = millis(); + + this->before_loop_tasks_(last_op_end_time); + + for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; + this->current_loop_index_++) { + Component *component = this->looping_components_[this->current_loop_index_]; + + // Update the cached time before each component runs + this->loop_component_start_time_ = last_op_end_time; + + { + this->set_current_component(component); + WarnIfComponentBlockingGuard guard{component, last_op_end_time}; + component->loop(); + // Use the finish method to get the current time as the end time + last_op_end_time = guard.finish(); + } + new_app_state |= component->get_component_state(); + this->app_state_ |= new_app_state; + this->feed_wdt(last_op_end_time); + } + + this->after_loop_tasks_(); + this->app_state_ = new_app_state; + +#ifdef USE_RUNTIME_STATS + // Process any pending runtime stats printing after all components have run + // This ensures stats printing doesn't affect component timing measurements + if (global_runtime_stats != nullptr) { + global_runtime_stats->process_pending_stats(last_op_end_time); + } +#endif + + // Use the last component's end time instead of calling millis() again + auto elapsed = last_op_end_time - this->last_loop_; + if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { + // Even if we overran the loop interval, we still need to select() + // to know if any sockets have data ready + this->yield_with_select_(0); + } else { + uint32_t delay_time = this->loop_interval_ - elapsed; + uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); + // next_schedule is max 0.5*delay_time + // otherwise interval=0 schedules result in constant looping with almost no sleep + next_schedule = std::max(next_schedule, delay_time / 2); + delay_time = std::min(next_schedule, delay_time); + + this->yield_with_select_(delay_time); + } + this->last_loop_ = last_op_end_time; + + if (this->dump_config_at_ < this->components_.size()) { + this->process_dump_config_(); + } +} + +// Inline yield_with_select_ for all paths except the select() fallback +#if !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) + // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. + // Safe because this runs on the main loop which owns socket lifetime (create, read, close). + if (delay_ms == 0) [[unlikely]] { + yield(); + return; + } + + // Check if any socket already has pending data before sleeping. + // If a socket still has unread data (rcvevent > 0) but the task notification was already + // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. + // This scan preserves select() semantics: return immediately when any fd is ready. + for (struct lwip_sock *sock : this->monitored_sockets_) { + if (esphome_lwip_socket_has_data(sock)) { + yield(); + return; + } + } + + // Sleep with instant wake via FreeRTOS task notification. + // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. + // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — + // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake + socket::socket_delay(delay_ms); +#else + // No select support, use regular delay + delay(delay_ms); +#endif +} +#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) + } // namespace esphome diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 7934fdbec9..ca4a2c8b6b 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -322,7 +322,9 @@ template class Automation; template class Trigger { public: /// Inform the parent automation that the event has triggered. - void trigger(const Ts &...x) { + // Force-inline: collapses the Trigger→Automation→ActionList forwarding + // chain into a single frame, reducing automation call stack depth. + inline void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -429,7 +431,9 @@ template class ActionList { this->add_action(action); } } - void play(const Ts &...x) { + // Force-inline: part of the Trigger→Automation→ActionList forwarding + // chain collapsed to reduce automation call stack depth. + inline void play(const Ts &...x) ESPHOME_ALWAYS_INLINE { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } @@ -473,7 +477,9 @@ template class Automation { void stop() { this->actions_.stop(); } - void trigger(const Ts &...x) { this->actions_.play(x...); } + // Force-inline: part of the Trigger→Automation→ActionList forwarding + // chain collapsed to reduce automation call stack depth. + inline void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 67e1755cc9..985f26e711 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -188,7 +188,7 @@ template class DelayAction : public Action, public Compon // Issue #10264: This is a workaround for parallel script delays interfering with each other. // Optimization: For no-argument delays (most common case), use direct lambda - // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + // to avoid overhead from capturing arguments by value if constexpr (sizeof...(Ts) == 0) { App.scheduler.set_timer_common_( this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, @@ -196,9 +196,9 @@ template class DelayAction : public Action, public Compon [this]() { this->play_next_(); }, /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } else { - // For delays with arguments, use std::bind to preserve argument values + // For delays with arguments, capture by value to preserve argument values // Arguments must be copied because original references may be invalid after delay - auto f = std::bind(&DelayAction::play_next_, this, x...); + auto f = [this, x...]() { this->play_next_(x...); }; App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(InternalSchedulerID::DELAY_ACTION), this->delay_.value(x...), std::move(f), diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 00dda0cc26..caaea89143 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -272,9 +272,6 @@ void Component::call() { break; } } -const LogString *Component::get_component_log_str() const { - return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; -} bool Component::should_warn_of_blocking(uint32_t blocking_time) { if (blocking_time > this->warn_if_blocking_over_) { // Prevent overflow when adding increment - if we're about to overflow, just max out @@ -378,9 +375,10 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: #pragma GCC diagnostic pop } bool Component::is_ready() const { - return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || - (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || - (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; + // Bitmask check: valid states are SETUP(1), LOOP(2), LOOP_DONE(4) + // (1 << state) & 0b10110 checks membership in one instruction + return ((1u << (this->component_state_ & COMPONENT_STATE_MASK)) & + ((1u << COMPONENT_STATE_SETUP) | (1u << COMPONENT_STATE_LOOP) | (1u << COMPONENT_STATE_LOOP_DONE))) != 0; } bool Component::can_proceed() { return true; } bool Component::set_status_flag_(uint8_t flag) { diff --git a/esphome/core/component.h b/esphome/core/component.h index 119681f64c..46cd77b034 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -294,7 +294,9 @@ class Component { * * Returns LOG_STR("") if source not set */ - const LogString *get_component_log_str() const; + const LogString *get_component_log_str() const { + return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; + } bool should_warn_of_blocking(uint32_t blocking_time); diff --git a/esphome/core/config.py b/esphome/core/config.py index e112720f2b..e02c6ec75f 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -587,7 +587,9 @@ async def _add_looping_components() -> None: @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: - cg.add_global(cg.global_ns.namespace("esphome").using) + # using namespace esphome is hardcoded in writer.py to guarantee it + # precedes all variable declarations regardless of coroutine priority. + # These can be used by user lambdas, put them to default scope # picolibc (IDF 6.0+) declares isnan in global scope, conflicting with using std::isnan cg.add_global(cg.RawStatement("#ifndef __PICOLIBC__")) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 09269ea1b4..d94b7e9f5d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -92,6 +92,7 @@ #define USE_LVGL_MSGBOX #define USE_LVGL_ROLLER #define USE_LVGL_ROTARY_ENCODER +#define USE_LVGL_SCALE #define USE_LVGL_SLIDER #define USE_LVGL_SPAN #define USE_LVGL_SPINBOX @@ -117,6 +118,7 @@ #define USE_NUMBER #define USE_OUTPUT #define USE_POWER_SUPPLY +#define USE_PREFERENCES_SYNC_EVERY_LOOP #define USE_QR_CODE #define USE_SAFE_MODE_CALLBACK #define USE_SELECT diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 00b447ebf2..1732fc72e8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -156,6 +156,32 @@ uint32_t fnv1_hash(const char *str) { return hash; } +// SplitMix32 — a fast, non-cryptographic PRNG from the SplitMix family +// (Steele et al., 2014). Uses a Weyl sequence with golden-ratio increment +// and the MurmurHash3 32-bit finalizer as output mixing function. +// Reference: https://doi.org/10.1145/2714064.2660195 +// Test results: https://lemire.me/blog/2017/08/22/testing-non-cryptographic-random-number-generators-my-results/ +// Seeded lazily from the platform's secure RNG via random_bytes(). +// ESP8266 uses os_random() instead (defined in esp8266/helpers.cpp). +#ifndef USE_ESP8266 +static uint32_t splitmix32_state; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +uint32_t random_uint32() { + // State of 0 means unseeded. The state will wrap back to 0 after 2^32 calls, + // triggering one extra random_bytes() call — an acceptable trade-off vs. adding + // a separate bool flag (4 bytes BSS + branch on every call). + if (splitmix32_state == 0) { + random_bytes(reinterpret_cast(&splitmix32_state), sizeof(splitmix32_state)); + splitmix32_state |= 1; // ensure non-zero seed + } + splitmix32_state += 0x9e3779b9u; + uint32_t z = splitmix32_state; + z = (z ^ (z >> 16)) * 0x85ebca6bu; + z = (z ^ (z >> 13)) * 0xc2b2ae35u; + return z ^ (z >> 16); +} +#endif + float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } // Strings @@ -837,7 +863,16 @@ bool mac_address_is_valid(const uint8_t *mac) { is_all_ones = false; } } - return !(is_all_zeros || is_all_ones); + if (is_all_zeros || is_all_ones) { + return false; + } + // Reject multicast MACs (bit 0 of first byte set) - device MACs must be unicast. + // This catches garbage data from corrupted eFuse custom MAC areas, which often + // has random values that would otherwise pass the all-zeros/all-ones check. + if (mac[0] & 0x01) { + return false; + } + return true; } void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a703b5a5f3..82c6b3833c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -842,10 +842,15 @@ template inline constexpr ESPHOME_ALWAYS_INLINE Ret } /// Return a random 32-bit unsigned integer. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. uint32_t random_uint32(); /// Return a random float between 0 and 1. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. float random_float(); -/// Generate \p len number of random bytes. +/// Generate \p len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG). +/// Thread-safe. Suitable for cryptographic use. bool random_bytes(uint8_t *data, size_t len); ///@} @@ -1757,7 +1762,10 @@ template struct Callback { // Safe under C++20 (P0593R6): byte copy into aligned storage implicitly // creates objects of implicit-lifetime types (trivially copyable qualifies). Callback cb; // fn and ctx are zero-initialized by default - __builtin_memcpy(&cb.ctx_, &callable, sizeof(DecayF)); + // Decay callable to a local variable first. When F is a function reference + // (e.g. void(&)(int)), &callable would point at machine code, not a pointer variable. + DecayF decayed = std::forward(callable); + __builtin_memcpy(&cb.ctx_, &decayed, sizeof(DecayF)); cb.fn_ = [](void *c, Ts... args) { alignas(DecayF) char buf[sizeof(DecayF)]; __builtin_memcpy(buf, &c, sizeof(DecayF)); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 51cbfb208e..9ee3b2fdd2 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -166,7 +166,13 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->component = component; item->set_name(name_type, static_name, hash_or_id); item->type = type; - item->callback = std::move(func); + // Use destroy + placement-new instead of move-assignment. + // GCC's std::function::operator=(function&&) does a full swap dance even when the + // target is empty. Since recycled/new items always have an empty callback, we can + // destroy the empty one (no-op) and move-construct directly, saving ~40 bytes of + // swap/destructor code on Xtensa. + item->callback.~function(); + new (&item->callback) std::function(std::move(func)); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use this->set_item_removed_(item, false); item->is_retry = is_retry; diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 6047202753..34ba2474b2 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -76,6 +76,15 @@ class StringRef { constexpr bool empty() const { return len_ == 0; } constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } + /// Copy characters to destination buffer (std::string::copy-like, but returns 0 instead of throwing on out-of-range) + size_type copy(char *dest, size_type count, size_type pos = 0) const { + if (pos >= len_) + return 0; + size_type actual = (count > len_ - pos) ? len_ - pos : count; + std::memcpy(dest, base_ + pos, actual); + return actual; + } + std::string str() const { return std::string(base_, len_); } const uint8_t *byte() const { return reinterpret_cast(base_); } diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 73ba0a9be7..6add82e7d1 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -283,19 +283,15 @@ void ESPTime::recalc_timestamp_local() { bool dst_valid = time::is_in_dst(utc_if_dst, tz); bool std_valid = !time::is_in_dst(utc_if_std, tz); - if (dst_valid && std_valid) { - // Ambiguous time (repeated hour during fall-back) - prefer standard time - this->timestamp = utc_if_std; - } else if (dst_valid) { + if (dst_valid && !std_valid) { // Only DST interpretation is valid this->timestamp = utc_if_dst; - } else if (std_valid) { - // Only standard interpretation is valid - this->timestamp = utc_if_std; } else { - // Invalid time (skipped hour during spring-forward) - // libc normalizes forward: 02:30 CST -> 08:30 UTC -> 03:30 CDT - // Using std offset achieves this since the UTC result falls during DST + // All other cases use standard offset: + // - Both valid (ambiguous fall-back repeated hour): prefer standard time + // - Only standard valid: straightforward + // - Neither valid (spring-forward skipped hour): std offset normalizes + // forward to match libc mktime(), e.g. 02:30 CST -> 03:30 CDT this->timestamp = utc_if_std; } #else diff --git a/esphome/core/time.h b/esphome/core/time.h index 874f0db4b4..1716c51ffd 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/defines.h" + #include #include #include diff --git a/esphome/coroutine.py b/esphome/coroutine.py index f5d512e510..3ce94cc979 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -63,7 +63,13 @@ class CoroPriority(enum.IntEnum): resolution during code generation. """ - # Platform initialization - must run first + # Early init - runs before platform init and before Application exists. + # Currently used only to connect logging so ESP_LOG* calls work + # immediately in all subsequent phases. + # Examples: logger (1100) + EARLY_INIT = 1100 + + # Platform initialization # Examples: esp32, esp8266, rp2040 PLATFORM = 1000 @@ -83,7 +89,7 @@ class CoroPriority(enum.IntEnum): CORE = 100 # Diagnostic and debugging systems - # Examples: logger (90) + # Examples: debug component (90) DIAGNOSTICS = 90 # Status and monitoring systems diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 5457485d25..a8efe96cce 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -565,6 +565,29 @@ def new_variable( return obj +def _extract_component_ns(type_str: str) -> str: + """Extract the component namespace from a fully-qualified C++ type string. + + Strips leading ``esphome::`` and template arguments, then returns + the first namespace segment. Falls back to ``"esphome"`` when the + type has no namespace qualifier (after stripping templates). + + Examples:: + + esphome::dsmr::Dsmr -> dsmr + esphome::logger::Logger -> logger + esphome::Automation, std::optional> -> esphome + Logger -> esphome + """ + bare = type_str.removeprefix("esphome::") + # Strip template arguments before namespace extraction to avoid + # matching :: inside template params (e.g. Automation>) + bare_no_template = bare.split("<", maxsplit=1)[0] + if "::" in bare_no_template: + return bare_no_template.split("::", maxsplit=1)[0].rstrip("_") + return "esphome" + + def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare a new pointer variable in the code generation. @@ -579,10 +602,43 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": obj = MockObj(id_, "->") if type_ is not None: id_.type = type_ - decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) - CORE.add_global(decl) - assignment = AssignmentExpression(None, None, id_, rhs) - CORE.add(assignment) + + if isinstance(rhs, MockObj) and rhs.is_new_expr: + # For 'new' allocations, use placement new into static storage + # to avoid heap fragmentation on embedded devices. + the_type = id_.type + # Extract component namespace from type for memory analysis attribution + component_ns = _extract_component_ns(str(the_type)) + storage_name = f"{component_ns}__{id_.id}__pstorage" + + # Declare aligned byte array for the object storage + CORE.add_global( + RawStatement( + f"alignas({the_type}) static unsigned char {storage_name}[sizeof({the_type})];" + ) + ) + CORE.add_global( + AssignmentExpression( + f"static {the_type}", + "*const ", + id_, + MockObj(f"reinterpret_cast<{the_type} *>({storage_name})"), + ) + ) + # Extract args from the CallExpression and rebuild as placement new. + # Template args are already encoded in the_type (e.g. GlobalsComponent), + # so we only pass the constructor args, not template_args. + call_expr = rhs.base + assert isinstance(call_expr, CallExpression), ( + f"Expected CallExpression for placement new, got {type(call_expr)}" + ) + placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args) + CORE.add(ExpressionStatement(placement_new)) + else: + decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) + CORE.add_global(decl) + CORE.add(AssignmentExpression(None, None, id_, rhs)) + CORE.register_variable(id_, obj) return obj @@ -799,11 +855,12 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - __slots__ = ("base", "op") + __slots__ = ("base", "op", "is_new_expr") - def __init__(self, base, op="."): + def __init__(self, base, op=".", is_new_expr=False) -> None: self.base = base self.op = op + self.is_new_expr = is_new_expr def __getattr__(self, attr: str) -> "MockObj": # prevent python dunder methods being replaced by mock objects @@ -818,7 +875,7 @@ class MockObj(Expression): def __call__(self, *args: SafeExpType) -> "MockObj": call = CallExpression(self.base, *args) - return MockObj(call, self.op) + return MockObj(call, self.op, is_new_expr=self.is_new_expr) def __str__(self): return str(self.base) @@ -832,7 +889,7 @@ class MockObj(Expression): @property def new(self) -> "MockObj": - return MockObj(f"new {self.base}", "->") + return MockObj(f"new {self.base}", "->", is_new_expr=True) def template(self, *args: SafeExpType) -> "MockObj": """Apply template parameters to this object.""" diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 620dc6131d..5cfa8532ff 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -77,3 +77,5 @@ dependencies: - if: "idf_version >=6.0.0 && target in [esp32s2, esp32s3, esp32p4]" esp32async/asynctcp: version: 3.4.91 + lvgl/lvgl: + version: 9.5.0 diff --git a/esphome/writer.py b/esphome/writer.py index fd4c811fb3..69a35d00e3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -381,7 +381,10 @@ def write_cpp(code_s): code_format = CPP_BASE_FORMAT copy_src_tree() + # using namespace esphome must precede all variable declarations since + # codegen types assume this namespace is in scope (esphome_ns = global_ns). global_s = '#include "esphome.h"\n' + global_s += "using namespace esphome;\n" global_s += CORE.cpp_global_section full_file = f"{code_format[0] + CPP_INCLUDE_BEGIN}\n{global_s}{CPP_INCLUDE_END}" diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index bba4bbf487..d0eab4e44e 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -24,6 +24,7 @@ except ImportError: from esphome import core from esphome.config_helpers import Extend, Remove +from esphome.const import CONF_DEFAULTS from esphome.core import ( CORE, DocumentRange, @@ -88,6 +89,47 @@ def make_data_base( return value +class ConfigContext: + """This is a mixin class that holds substitution vars that should be applied + to the tagged node and its children. During configuration loading, context vars can + be added to nodes using `add_context` function, which applies the mixin storing + the captured values and unevaluated expressions. + The substitution pass then recreates the effective context by merging the context vars + from this node and parent nodes. + """ + + @property + def vars(self) -> dict[str, Any]: + return self._context_vars + + def set_context(self, vars: dict[str, Any]) -> None: + # pylint: disable=attribute-defined-outside-init + self._context_vars = vars + + +def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any: + """Tags a list/string/dict value with context vars that must be applied to it and its children + during the substitution pass. If no vars are given, no tagging is done. + If the value is already tagged, the new context vars are merged with existing ones, + with new vars taking precedence. Returns the value tagged with ConfigContext. Returns + the original value if value is not a list/string/dict. + """ + if isinstance(value, dict) and CONF_DEFAULTS in value: + context_vars = { + **value.pop(CONF_DEFAULTS), + **(context_vars or {}), + } + + if isinstance(value, ConfigContext): + value.set_context({**value.vars, **(context_vars or {})}) + return value + + if context_vars and isinstance(value, (dict, list, str)): + value = add_class_to_obj(value, ConfigContext) + value.set_context(context_vars) + return value + + def _add_data_ref(fn): @functools.wraps(fn) def wrapped(loader, node): @@ -455,7 +497,7 @@ def parse_yaml( def substitute_vars(config, vars): from esphome.components import substitutions - from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS + from esphome.const import CONF_SUBSTITUTIONS org_subs = None result = config @@ -612,6 +654,12 @@ class ESPHomeDumper(yaml.SafeDumper): return self.represent_secret(value.value) return self.represent_scalar(tag="!lambda", value=value.value, style="|") + def represent_extend(self, value): + return self.represent_scalar(tag="!extend", value=value.value) + + def represent_remove(self, value): + return self.represent_scalar(tag="!remove", value=value.value) + def represent_id(self, value): if is_secret(value.id): return self.represent_secret(value.id) @@ -638,6 +686,8 @@ ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_string ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) +ESPHomeDumper.add_multi_representer(Extend, ESPHomeDumper.represent_extend) +ESPHomeDumper.add_multi_representer(Remove, ESPHomeDumper.represent_remove) ESPHomeDumper.add_multi_representer(core.ID, ESPHomeDumper.represent_id) ESPHomeDumper.add_multi_representer(uuid.UUID, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Path, ESPHomeDumper.represent_stringify) diff --git a/platformio.ini b/platformio.ini index 3c3d62ef76..d3a482b652 100644 --- a/platformio.ini +++ b/platformio.ini @@ -42,7 +42,6 @@ lib_deps_base = https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library - lvgl/lvgl@8.4.0 ; lvgl lib_deps = ${common.lib_deps_base} @@ -120,6 +119,7 @@ lib_deps = ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) droscy/esp_wireguard@0.4.2 ; wireguard + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} @@ -204,6 +204,7 @@ lib_deps = ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} -DUSE_RP2040 @@ -221,6 +222,7 @@ lib_deps = bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.2 ; wireguard + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common:arduino.build_flags} -DUSE_LIBRETINY @@ -242,6 +244,7 @@ build_flags = lib_deps = ${common.lib_deps_base} bblanchon/ArduinoJson@7.4.2 ; json + lvgl/lvgl@9.5.0 ; lvgl ; All the actual environments are defined below. @@ -543,6 +546,7 @@ extends = common platform = platformio/native lib_deps = esphome/noise-c@0.1.11 ; used by api + lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} -DUSE_HOST diff --git a/requirements.txt b/requirements.txt index f8f60f1932..2e09e2ed99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.6.0 +aioesphomeapi==44.7.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import diff --git a/requirements_test.txt b/requirements_test.txt index acd8383a2f..0d3f0671f5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.6 # also change in .pre-commit-config.yaml when updating +ruff==0.15.7 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index dff6c7690a..221ccc8d69 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -254,14 +254,17 @@ class TypeInfo(ABC): def dump(self, name: str) -> str: """Dump the value to the output.""" + def calculate_tag(self) -> int: + """Calculate the protobuf tag (field_id << 3 | wire_type).""" + return (self.number << 3) | (self.wire_type & 0b111) + def calculate_field_id_size(self) -> int: """Calculates the size of a field ID in bytes. Returns: The number of bytes needed to encode the field ID """ - # Calculate the tag by combining field_id and wire_type - tag = (self.number << 3) | (self.wire_type & 0b111) + tag = self.calculate_tag() # Calculate the varint size if tag < 128: @@ -556,6 +559,16 @@ class Fixed32Type(TypeInfo): o += "out.append(buffer);" return o + @property + def encode_content(self) -> str: + tag = self.calculate_tag() + if self.force and tag < 128: + # Emit combined tag+value write: precomputed tag + direct memcpy + return f"buffer.write_tag_and_fixed32({tag}, this->{self.field_name});" + if self.force: + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() if force: @@ -2595,7 +2608,7 @@ def build_service_message_type( is_empty = not has_fields if is_empty: EMPTY_MESSAGES.add(mt.name) - hout += f"virtual void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n" + hout += f"void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n" case = "" if not is_empty: case += f"{mt.name} msg;\n" @@ -2947,6 +2960,7 @@ namespace esphome::api { cpp = FILE_HEADER cpp += """\ #include "api_pb2_service.h" +#include "api_connection.h" #include "esphome/core/log.h" namespace esphome::api { @@ -2957,7 +2971,7 @@ static const char *const TAG = "api.service"; class_name = "APIServerConnectionBase" - hpp += f"class {class_name} : public ProtoService {{\n" + hpp += f"class {class_name} {{\n" hpp += " public:\n" # Add logging helper method declarations @@ -3050,11 +3064,11 @@ static const char *const TAG = "api.service"; result += "#endif\n" return result - # Generate read_message with auth check before dispatch - hpp += " protected:\n" - hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" + # Generate read_message_ as APIConnection method (not base class) so the compiler + # can devirtualize and inline the on_* handler calls within the same class. + # APIConnection declares this method in api_connection.h. - out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" + out = "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n" # Auth check block before dispatch switch out += " // Check authentication/connection requirements\n" diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index 5594d64240..f8bab39414 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse from functools import partial +import os from pathlib import Path import sys @@ -35,6 +36,7 @@ PLATFORMIO_OPTIONS = { def run_tests(selected_components: list[str]) -> int: + os.environ["ASAN_OPTIONS"] = "detect_leaks=0" return build_and_run( selected_components=selected_components, tests_dir=COMPONENTS_TESTS_DIR, diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 9f32238780..d94d472c9e 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -111,11 +111,13 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset( "esp32", # ESP32 platform implementation "esp8266", # ESP8266 platform implementation "rp2040", # Raspberry Pi Pico / RP2040 platform implementation + "libretiny", # LibreTiny base platform implementation "bk72xx", # Beken BK72xx platform implementation (uses LibreTiny) "rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny) "ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny) "host", # Host platform (for testing on development machine) "nrf52", # Nordic nRF52 platform implementation (uses Zephyr) + "zephyr", # Zephyr RTOS platform implementation } ) diff --git a/script/setup_codspeed_lib.py b/script/setup_codspeed_lib.py index 959c89d05b..4f5d1bff24 100755 --- a/script/setup_codspeed_lib.py +++ b/script/setup_codspeed_lib.py @@ -205,7 +205,13 @@ def setup_codspeed_lib(output_dir: Path) -> None: if hooks_dist_c.exists(): _copy_if_missing(hooks_dist_c, lib_src / "instrument_hooks.c") - # 4. Write library.json + # 4. Copy instrument-hooks headers (core.h, callgrind.h, valgrind.h) next to + # measurement.hpp so they are found before any same-named headers from + # other libraries (e.g. libsodium's core.h). + for header in hooks_include.glob("*.h"): + _copy_if_missing(header, core_include / header.name) + + # 5. Write library.json version = _read_codspeed_version(output_dir / CORE_CMAKE) _write_library_json( benchmark_dir, core_include, hooks_include, version, project_root diff --git a/tests/benchmarks/components/api/__init__.py b/tests/benchmarks/components/api/__init__.py new file mode 100644 index 0000000000..0687c3f87f --- /dev/null +++ b/tests/benchmarks/components/api/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # api must run its to_code to define USE_API, USE_API_PLAINTEXT, + # and add the noise-c library dependency. + manifest.enable_codegen() diff --git a/tests/benchmarks/components/api/bench_noise_encrypt.cpp b/tests/benchmarks/components/api/bench_noise_encrypt.cpp new file mode 100644 index 0000000000..223e6ada0d --- /dev/null +++ b/tests/benchmarks/components/api/bench_noise_encrypt.cpp @@ -0,0 +1,177 @@ +#include "esphome/core/defines.h" +#ifdef USE_API_NOISE + +#include +#include +#include + +#include "noise/protocol.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to create and initialize a NoiseCipherState with ChaChaPoly. +// Returns nullptr on failure. +static NoiseCipherState *create_cipher() { + NoiseCipherState *cipher = nullptr; + int err = noise_cipherstate_new_by_id(&cipher, NOISE_CIPHER_CHACHAPOLY); + if (err != NOISE_ERROR_NONE || cipher == nullptr) + return nullptr; + + // Initialize with a dummy 32-byte key (same pattern as handshake split produces) + uint8_t key[32]; + memset(key, 0xAB, sizeof(key)); + err = noise_cipherstate_init_key(cipher, key, sizeof(key)); + if (err != NOISE_ERROR_NONE) { + noise_cipherstate_free(cipher); + return nullptr; + } + return cipher; +} + +// Benchmark helper matching the exact pattern from +// APINoiseFrameHelper::write_protobuf_messages: +// - noise_buffer_init + noise_buffer_set_inout (same as production) +// - No explicit set_nonce (production relies on internal nonce increment) +// - Error checking on encrypt return +static void noise_encrypt_bench(benchmark::State &state, size_t plaintext_size) { + NoiseCipherState *cipher = create_cipher(); + if (cipher == nullptr) { + state.SkipWithError("Failed to create cipher state"); + return; + } + + size_t mac_len = noise_cipherstate_get_mac_length(cipher); + size_t buf_capacity = plaintext_size + mac_len; + auto buffer = std::make_unique(buf_capacity); + memset(buffer.get(), 0x42, plaintext_size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + // Match production: init buffer, set inout, encrypt + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buffer.get(), plaintext_size, buf_capacity); + + int err = noise_cipherstate_encrypt(cipher, &mbuf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("noise_cipherstate_encrypt failed"); + noise_cipherstate_free(cipher); + return; + } + } + benchmark::DoNotOptimize(buffer[0]); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + noise_cipherstate_free(cipher); +} + +// --- Encrypt a typical sensor state message (small payload ~14 bytes) --- +// This is the most common message encrypted on every sensor update. +// 4 bytes type+len header + ~10 bytes payload. + +static void NoiseEncrypt_SmallMessage(benchmark::State &state) { noise_encrypt_bench(state, 14); } +BENCHMARK(NoiseEncrypt_SmallMessage); + +// --- Encrypt a medium message (~128 bytes, typical for LightStateResponse) --- + +static void NoiseEncrypt_MediumMessage(benchmark::State &state) { noise_encrypt_bench(state, 128); } +BENCHMARK(NoiseEncrypt_MediumMessage); + +// --- Encrypt a large message (~1024 bytes, typical for DeviceInfoResponse) --- + +static void NoiseEncrypt_LargeMessage(benchmark::State &state) { noise_encrypt_bench(state, 1024); } +BENCHMARK(NoiseEncrypt_LargeMessage); + +// Benchmark helper matching the exact pattern from +// APINoiseFrameHelper::read_packet: +// - noise_buffer_init + noise_buffer_set_inout with capacity == size (decrypt shrinks) +// - Error checking on decrypt return +// +// Pre-encrypts kInnerIterations messages with sequential nonces before the +// timed loop. Each outer iteration re-inits the decrypt key to reset the +// nonce back to 0, then decrypts all pre-encrypted messages in sequence. +// The init_key cost is amortized over kInnerIterations decrypts. +static void noise_decrypt_bench(benchmark::State &state, size_t plaintext_size) { + NoiseCipherState *encrypt_cipher = create_cipher(); + NoiseCipherState *decrypt_cipher = create_cipher(); + if (encrypt_cipher == nullptr || decrypt_cipher == nullptr) { + state.SkipWithError("Failed to create cipher state"); + if (encrypt_cipher) + noise_cipherstate_free(encrypt_cipher); + if (decrypt_cipher) + noise_cipherstate_free(decrypt_cipher); + return; + } + + size_t mac_len = noise_cipherstate_get_mac_length(encrypt_cipher); + size_t encrypted_size = plaintext_size + mac_len; + + // Pre-encrypt kInnerIterations messages with sequential nonces (0..N-1). + auto ciphertexts = std::make_unique(encrypted_size * kInnerIterations); + for (int i = 0; i < kInnerIterations; i++) { + uint8_t *ct = ciphertexts.get() + i * encrypted_size; + memset(ct, 0x42, plaintext_size); + NoiseBuffer enc_buf; + noise_buffer_init(enc_buf); + noise_buffer_set_inout(enc_buf, ct, plaintext_size, encrypted_size); + int err = noise_cipherstate_encrypt(encrypt_cipher, &enc_buf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Pre-encrypt failed"); + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); + return; + } + } + + // Working buffer — decrypt modifies in place + auto buffer = std::make_unique(encrypted_size); + static constexpr uint8_t KEY[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB}; + + for (auto _ : state) { + // Reset nonce to 0 by re-initing the key (amortized over kInnerIterations) + noise_cipherstate_init_key(decrypt_cipher, KEY, sizeof(KEY)); + + for (int i = 0; i < kInnerIterations; i++) { + // Copy ciphertext into working buffer (decrypt modifies in place) + memcpy(buffer.get(), ciphertexts.get() + i * encrypted_size, encrypted_size); + + // Decrypt matching production pattern + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buffer.get(), encrypted_size, encrypted_size); + + int err = noise_cipherstate_decrypt(decrypt_cipher, &mbuf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("noise_cipherstate_decrypt failed"); + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); + return; + } + } + benchmark::DoNotOptimize(buffer[0]); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); +} + +// --- Decrypt benchmarks (matching read_packet path) --- + +static void NoiseDecrypt_SmallMessage(benchmark::State &state) { noise_decrypt_bench(state, 14); } +BENCHMARK(NoiseDecrypt_SmallMessage); + +static void NoiseDecrypt_MediumMessage(benchmark::State &state) { noise_decrypt_bench(state, 128); } +BENCHMARK(NoiseDecrypt_MediumMessage); + +static void NoiseDecrypt_LargeMessage(benchmark::State &state) { noise_decrypt_bench(state, 1024); } +BENCHMARK(NoiseDecrypt_LargeMessage); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_NOISE diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp new file mode 100644 index 0000000000..79bffaf953 --- /dev/null +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -0,0 +1,162 @@ +#include "esphome/core/defines.h" +#ifdef USE_API_PLAINTEXT + +#include +#include +#include +#include +#include +#include + +#include "esphome/components/api/api_frame_helper_plaintext.h" +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to drain accumulated data from the read side of a socket +// to prevent the write side from blocking. +static void drain_socket(int fd) { + char buf[65536]; + while (::read(fd, buf, sizeof(buf)) > 0) { + } +} + +// Helper to create a TCP loopback connection with an APIPlaintextFrameHelper +// on the write end. Returns the helper and the read-side fd. +// Uses real TCP sockets so TCP_NODELAY succeeds during init(). +static std::pair, int> create_plaintext_helper() { + // Create a TCP listener on loopback + int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); + int opt = 1; + ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // OS-assigned port + ::bind(listen_fd, reinterpret_cast(&addr), sizeof(addr)); + ::listen(listen_fd, 1); + + // Get the assigned port + socklen_t addr_len = sizeof(addr); + ::getsockname(listen_fd, reinterpret_cast(&addr), &addr_len); + + // Connect from client side + int write_fd = ::socket(AF_INET, SOCK_STREAM, 0); + ::connect(write_fd, reinterpret_cast(&addr), sizeof(addr)); + + // Accept on server side (this is our read fd) + int read_fd = ::accept(listen_fd, nullptr, nullptr); + ::close(listen_fd); + + // Make both ends non-blocking + int flags = ::fcntl(write_fd, F_GETFL, 0); + ::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK); + flags = ::fcntl(read_fd, F_GETFL, 0); + ::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK); + + // Increase socket buffer sizes to reduce drain frequency + int bufsize = 1024 * 1024; + ::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); + ::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); + + auto sock = std::make_unique(write_fd); + auto helper = std::make_unique(std::move(sock)); + helper->init(); + + return {std::move(helper), read_fd}; +} + +// --- Write a single SensorStateResponse through plaintext framing --- +// Measures the full write path: header construction, varint encoding, +// iovec assembly, and socket write. + +static void PlaintextFrame_WriteSensorState(benchmark::State &state) { + auto [helper, read_fd] = create_plaintext_helper(); + uint8_t padding = helper->frame_header_padding(); + + // Pre-init buffer to typical TCP MSS size to avoid benchmarking + // heap allocation — in real use the buffer is reused across writes. + APIBuffer buffer; + buffer.reserve(1460); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + buffer.clear(); + SensorStateResponse msg; + msg.key = 0x12345678; + msg.state = 23.5f; + msg.missing_state = false; + + uint32_t size = msg.calculate_size(); + buffer.resize(padding + size); + ProtoWriteBuffer writer(&buffer, padding); + msg.encode(writer); + + helper->write_protobuf_packet(SensorStateResponse::MESSAGE_TYPE, writer); + + if ((i & 0xFF) == 0) + drain_socket(read_fd); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(helper.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(PlaintextFrame_WriteSensorState); + +// --- Write a batch of 5 SensorStateResponses in one call --- +// Measures batched write: multiple messages assembled into one writev. + +static void PlaintextFrame_WriteBatch5(benchmark::State &state) { + auto [helper, read_fd] = create_plaintext_helper(); + uint8_t padding = helper->frame_header_padding(); + uint8_t footer = helper->frame_footer_size(); + + // Pre-init buffer to typical TCP MSS size to avoid benchmarking + // heap allocation — in real use the buffer is reused across writes. + APIBuffer buffer; + buffer.reserve(1460); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + buffer.clear(); + MessageInfo messages[5] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + + for (int j = 0; j < 5; j++) { + uint16_t offset = buffer.size(); + SensorStateResponse msg; + msg.key = static_cast(j); + msg.state = 23.5f + static_cast(j); + msg.missing_state = false; + + uint32_t size = msg.calculate_size(); + buffer.resize(offset + padding + size + footer); + ProtoWriteBuffer writer(&buffer, offset + padding); + msg.encode(writer); + + messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size); + } + + helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span(messages, 5)); + + if ((i & 0xFF) == 0) + drain_socket(read_fd); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(helper.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(PlaintextFrame_WriteBatch5); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_PLAINTEXT diff --git a/tests/benchmarks/components/api/benchmark.yaml b/tests/benchmarks/components/api/benchmark.yaml index bfc24d7440..e57276ea66 100644 --- a/tests/benchmarks/components/api/benchmark.yaml +++ b/tests/benchmarks/components/api/benchmark.yaml @@ -108,6 +108,7 @@ esphome: area_id: area_20 api: + encryption: sensor: binary_sensor: light: diff --git a/tests/benchmarks/components/binary_sensor/__init__.py b/tests/benchmarks/components/binary_sensor/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp b/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp new file mode 100644 index 0000000000..8bae943e2e --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/bench_binary_sensor_publish.cpp @@ -0,0 +1,61 @@ +#include + +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome::binary_sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Benchmark: publish_state with alternating values (forces state change every time) +static void BinarySensorPublish_Alternating(benchmark::State &state) { + BinarySensor sensor; + + // First publish to establish initial state + sensor.publish_initial_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_Alternating); + +// Benchmark: publish_state with same value (tests dedup fast path) +static void BinarySensorPublish_NoChange(benchmark::State &state) { + BinarySensor sensor; + + sensor.publish_initial_state(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(true); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_NoChange); + +// Benchmark: publish_state with a callback registered +static void BinarySensorPublish_WithCallback(benchmark::State &state) { + BinarySensor sensor; + + int callback_count = 0; + sensor.add_on_state_callback([&callback_count](bool) { callback_count++; }); + + sensor.publish_initial_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(BinarySensorPublish_WithCallback); + +} // namespace esphome::binary_sensor::benchmarks diff --git a/tests/benchmarks/components/binary_sensor/benchmark.yaml b/tests/benchmarks/components/binary_sensor/benchmark.yaml new file mode 100644 index 0000000000..fc0db6c52c --- /dev/null +++ b/tests/benchmarks/components/binary_sensor/benchmark.yaml @@ -0,0 +1 @@ +binary_sensor: diff --git a/tests/benchmarks/components/main.cpp b/tests/benchmarks/components/main.cpp index 9bc0c31a15..901dc44c07 100644 --- a/tests/benchmarks/components/main.cpp +++ b/tests/benchmarks/components/main.cpp @@ -26,7 +26,7 @@ void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0); + static esphome::logger::Logger test_logger(0, 64); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/benchmarks/components/mdns/__init__.py b/tests/benchmarks/components/mdns/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/mdns/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/network/__init__.py b/tests/benchmarks/components/network/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/network/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/sensor/__init__.py b/tests/benchmarks/components/sensor/__init__.py new file mode 100644 index 0000000000..5a593aa8c2 --- /dev/null +++ b/tests/benchmarks/components/sensor/__init__.py @@ -0,0 +1,12 @@ +import esphome.codegen as cg +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Sensor filter benchmarks need USE_SENSOR_FILTER defined. + # We use a custom to_code instead of enable_codegen() to avoid + # pulling in the full sensor component setup. + async def to_code(config): + cg.add_define("USE_SENSOR_FILTER") + + manifest.to_code = to_code diff --git a/tests/benchmarks/components/sensor/bench_sensor_filter.cpp b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp new file mode 100644 index 0000000000..e4aa397690 --- /dev/null +++ b/tests/benchmarks/components/sensor/bench_sensor_filter.cpp @@ -0,0 +1,78 @@ +#include + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensor/filter.h" + +namespace esphome::sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Benchmark: sensor publish through a SlidingWindowMovingAverageFilter (window=5, send_every=1) +static void SensorFilter_SlidingWindowAvg(benchmark::State &state) { + Sensor sensor; + + // Create filter: window_size=5, send_every=1, send_first_at=1 + auto *filter = new SlidingWindowMovingAverageFilter(5, 1, 1); + sensor.add_filter(filter); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_SlidingWindowAvg); + +// Benchmark: sensor publish through ExponentialMovingAverageFilter +static void SensorFilter_ExponentialMovingAvg(benchmark::State &state) { + Sensor sensor; + + // alpha=0.1, send_every=1, send_first_at=1 + auto *filter = new ExponentialMovingAverageFilter(0.1f, 1, 1); + sensor.add_filter(filter); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_ExponentialMovingAvg); + +// Benchmark: sensor publish through a chain of 3 filters (offset + multiply + sliding window) +static void SensorFilter_Chain3(benchmark::State &state) { + Sensor sensor; + + sensor.add_filters({ + new OffsetFilter(1.0f), + new MultiplyFilter(2.0f), + new SlidingWindowMovingAverageFilter(5, 1, 1), + }); + + float value = 0.0f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(value); + value += 0.1f; + if (value > 1000.0f) + value = 0.0f; + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SensorFilter_Chain3); + +} // namespace esphome::sensor::benchmarks diff --git a/tests/benchmarks/components/sensor/benchmark.yaml b/tests/benchmarks/components/sensor/benchmark.yaml new file mode 100644 index 0000000000..e1fb52cdd6 --- /dev/null +++ b/tests/benchmarks/components/sensor/benchmark.yaml @@ -0,0 +1 @@ +sensor: diff --git a/tests/benchmarks/components/socket/__init__.py b/tests/benchmarks/components/socket/__init__.py new file mode 100644 index 0000000000..7a20f9f230 --- /dev/null +++ b/tests/benchmarks/components/socket/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # socket must run its to_code to define USE_SOCKET_IMPL_BSD_SOCKETS + # which is needed by the api frame helper benchmarks. + manifest.enable_codegen() diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 10d7f80834..4f41f2cc70 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -15,7 +15,7 @@ def test_binary_sensor_is_setup(generate_main): ) # Then - assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "static gpio::GPIOBinarySensor *const" in main_cpp assert "App.register_binary_sensor" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index a35994a682..544e748f91 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -13,7 +13,8 @@ def test_button_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert "new wake_on_lan::WakeOnLanButton();" in main_cpp + assert "static wake_on_lan::WakeOnLanButton *const" in main_cpp + assert ") wake_on_lan::WakeOnLanButton();" in main_cpp assert "App.register_button" in main_cpp assert "App.register_component" in main_cpp diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index 0641e698e9..763628f57c 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -134,7 +134,7 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: CORE.config_path = Path(path) CORE.config = read_config({}) generate_cpp_contents(CORE.config) - return CORE.cpp_main_section + return CORE.cpp_global_section + CORE.cpp_main_section yield generator diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 41ddd72feb..8c1278a332 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -7,7 +7,11 @@ def test_deep_sleep_setup(generate_main): """ main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") - assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp + assert ( + "static deep_sleep::DeepSleepComponent *const deepsleep = reinterpret_cast(deep_sleep__deepsleep__pstorage);" + in main_cpp + ) + assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp assert "App.register_component_(deepsleep);" in main_cpp diff --git a/tests/component_tests/globals/__init__.py b/tests/component_tests/globals/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/globals/config/globals_test.yaml b/tests/component_tests/globals/config/globals_test.yaml new file mode 100644 index 0000000000..1d1a9edaa6 --- /dev/null +++ b/tests/component_tests/globals/config/globals_test.yaml @@ -0,0 +1,16 @@ +esphome: + name: test + +esp32: + board: esp32dev + +globals: + - id: my_global_int + type: int + initial_value: "42" + - id: my_global_float + type: float + initial_value: "1.5" + - id: my_global_bool + type: bool + initial_value: "true" diff --git a/tests/component_tests/globals/test_globals.py b/tests/component_tests/globals/test_globals.py new file mode 100644 index 0000000000..04fd6d5f7d --- /dev/null +++ b/tests/component_tests/globals/test_globals.py @@ -0,0 +1,27 @@ +"""Tests for the globals component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_globals_placement_new_with_template_args( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that globals uses placement new with template arguments preserved.""" + main_cpp = generate_main(component_config_path("globals_test.yaml")) + + # Globals uses Pvariable with Type.new(template_args, initial_value) + # which exercises the template_args preservation in placement new. + assert "static globals::GlobalsComponent *const my_global_int" in main_cpp + assert "sizeof(globals::GlobalsComponent)" in main_cpp + assert "new(my_global_int) globals::GlobalsComponent" in main_cpp + + # Verify initial value is passed as constructor arg + assert "42" in main_cpp + + # Check other globals are also generated + assert "sizeof(globals::GlobalsComponent)" in main_cpp + assert "sizeof(globals::GlobalsComponent)" in main_cpp diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py index 73665dc45d..f336a9105e 100644 --- a/tests/component_tests/gpio/test_gpio_binary_sensor.py +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -16,7 +16,8 @@ def test_gpio_binary_sensor_basic_setup( """ main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml") - assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "static gpio::GPIOBinarySensor *const" in main_cpp + assert ") gpio::GPIOBinarySensor();" in main_cpp assert "App.register_binary_sensor" in main_cpp # set_use_interrupt(true) should NOT be generated (uses C++ default) assert "bs_gpio->set_use_interrupt(true);" not in main_cpp diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index c9481a0e1d..6f73888c7d 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -242,7 +242,15 @@ def test_image_generation( main_cpp = generate_main(component_config_path("image_test.yaml")) assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp assert ( - "cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" + "alignas(image::Image) static unsigned char image__cat_img__pstorage[sizeof(image::Image)];" + in main_cpp + ) + assert ( + "static image::Image *const cat_img = reinterpret_cast(image__cat_img__pstorage);" + in main_cpp + ) + assert ( + "new(cat_img) image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);" in main_cpp ) diff --git a/tests/component_tests/logger/test_logger.py b/tests/component_tests/logger/test_logger.py index 98aa741964..94a6f7ac7b 100644 --- a/tests/component_tests/logger/test_logger.py +++ b/tests/component_tests/logger/test_logger.py @@ -22,6 +22,10 @@ def test_logger_pre_setup_before_other_components(generate_main): # Find all "new " allocations (component creation) new_allocations = list(re.finditer(r"\bnew [\w:]+", main_cpp)) + # Find all "new(" allocations (component creation) and combine them + new_allocations.extend(re.finditer(r"\bnew\([^)]+\) [\w:]+", main_cpp)) + # Sort allocations by position in the file + new_allocations.sort(key=lambda m: m.start()) assert len(new_allocations) > 0, "No component allocations found" # Separate logger and non-logger allocations diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index 92f56b5451..1ae8cc644e 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -119,7 +119,15 @@ def test_code_generation( main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml")) assert ( - "p4_nano = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + "alignas(mipi_dsi::MIPI_DSI) static unsigned char mipi_dsi__p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];" + in main_cpp + ) + assert ( + "static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast(mipi_dsi__p4_nano__pstorage);" + in main_cpp + ) + assert ( + "new(p4_nano) mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" in main_cpp ) assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp diff --git a/tests/component_tests/status_led/__init__.py b/tests/component_tests/status_led/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/status_led/config/status_led_test.yaml b/tests/component_tests/status_led/config/status_led_test.yaml new file mode 100644 index 0000000000..c86197d225 --- /dev/null +++ b/tests/component_tests/status_led/config/status_led_test.yaml @@ -0,0 +1,8 @@ +esphome: + name: test + +esp32: + board: esp32dev + +status_led: + pin: GPIO2 diff --git a/tests/component_tests/status_led/test_status_led.py b/tests/component_tests/status_led/test_status_led.py new file mode 100644 index 0000000000..f7e0a9de86 --- /dev/null +++ b/tests/component_tests/status_led/test_status_led.py @@ -0,0 +1,23 @@ +"""Tests for status_led.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + + +def test_status_led_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test status_led generation.""" + main_cpp = generate_main(component_config_path("status_led_test.yaml")) + assert ( + "alignas(status_led::StatusLED) static unsigned char status_led__status_led_statusled_id__pstorage[sizeof(status_led::StatusLED)];" + in main_cpp + ) + assert ( + "static status_led::StatusLED *const status_led_statusled_id = reinterpret_cast(status_led__status_led_statusled_id__pstorage);" + in main_cpp + ) + assert "new(status_led_statusled_id) status_led::StatusLED(" in main_cpp diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index c74dfb8a47..63eb4f1951 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -13,7 +13,8 @@ def test_text_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "new template_::TemplateText();" in main_cpp + assert "static template_::TemplateText *const" in main_cpp + assert ") template_::TemplateText();" in main_cpp assert "App.register_text" in main_cpp diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1ff31ab96b..ae094fadf8 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -13,7 +13,8 @@ def test_text_sensor_is_setup(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert "new template_::TemplateTextSensor();" in main_cpp + assert "static template_::TemplateTextSensor *const" in main_cpp + assert ") template_::TemplateTextSensor();" in main_cpp assert "App.register_text_sensor" in main_cpp diff --git a/tests/components/esp32/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml index 7a3bbe55b3..b9a3b804a8 100644 --- a/tests/components/esp32/test.esp32-s3-idf.yaml +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -1,5 +1,10 @@ esp32: variant: esp32s3 + partitions: + - name: my_data + type: data + subtype: spiffs + size: 0x1000 framework: type: esp-idf advanced: diff --git a/tests/components/ld2450/common.h b/tests/components/ld2450/common.h index 9f9e7b3e9f..304634edca 100644 --- a/tests/components/ld2450/common.h +++ b/tests/components/ld2450/common.h @@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(uart::FlushResult, flush, (), (override)); + MOCK_METHOD(uart::UARTFlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 3635fc710f..606f57d6a1 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -37,11 +37,20 @@ lvgl: on_resume: logger.log: LVGL has resumed on_boot: - logger.log: LVGL has started + - logger.log: LVGL has started + - lvgl.indicator.update: + id: meter_arc_indicator + start_value: 0 + end_value: 180 bg_color: light_blue disp_bg_color: color_id disp_bg_image: cat_image disp_bg_opa: cover + bottom_layer: + widgets: + - obj: + bg_color: 0x000000 + bg_opa: cover theme: obj: border_width: 1 @@ -49,12 +58,14 @@ lvgl: gradients: - id: color_bar direction: hor - dither: err_diff + # dither: err_diff stops: - color: 0xFF0000 position: 0 + opa: 100% - color: 0xFFFF00 position: 42 + opa: 80% - color: 0x00FF00 position: 84 - color: 0x00FFFF @@ -156,6 +167,16 @@ lvgl: offset_x: !lambda return 20; offset_y: !lambda return 20; antialias: !lambda return true; + - id: msgbox_with_header_buttons + title: Header Buttons Test + body: + text: Testing header buttons + header_buttons: + - src: cat_image + on_click: + logger.log: Header button clicked + buttons: + - text: OK - id: simple_msgbox title: Simple @@ -199,11 +220,11 @@ lvgl: text: Unloaded - lvgl.label.update: id: msgbox_label - text: "" # Empty text + text: "" # Empty text on_all_events: logger.log: format: "Event %s" - args: ['lv_event_code_name_for(event->code).c_str()'] + args: ['lv_event_code_name_for(event).c_str()'] skip: true layout: type: Flex @@ -241,7 +262,7 @@ lvgl: on_all_events: - logger.log: format: "Event %s" - args: ['lv_event_code_name_for(event->code).c_str()'] + args: ['lv_event_code_name_for(event).c_str()'] - lvgl.animimg.update: id: anim_img src: !lambda "return {dog_image, cat_image};" @@ -306,7 +327,6 @@ lvgl: anim_time: 1s bg_color: light_blue bg_grad_color: light_blue - bg_dither_mode: ordered bg_grad_dir: hor bg_grad_stop: 128 bg_image_opa: transp @@ -354,10 +374,18 @@ lvgl: text_line_space: 4 text_opa: cover transform_angle: 180 + transform_rotation: 90 transform_height: 100 transform_pivot_x: 50% transform_pivot_y: 50% transform_zoom: 0.5 + transform_scale: 2.0 + transform_scale_x: 1.5 + transform_scale_y: 0.8 + transform_skew_x: 10 + transform_skew_y: 20 + shadow_offset_x: 3 + shadow_offset_y: 3 translate_x: 10 translate_y: 10 max_height: 100 @@ -549,14 +577,16 @@ lvgl: arc_length: 120 spin_time: 2s align: left_mid + - spinner: + align: right_mid + send_draw_task_events: true - image: id: lv_image src: cat_image align: top_left y: "50" - mode: real - zoom: 2.0 - angle: 45 + scale: 2.0 + rotation: 45 - tileview: id: tileview_id scrollbar_mode: active @@ -661,8 +691,11 @@ lvgl: src: cat_image x: 100 y: 100 - angle: 90 - zoom: 2.0 + rotation: 90 + scale_x: 2.0 + scale_y: 1.5 + skew_x: 10 + skew_y: 5 pivot_x: 25 pivot_y: 25 - lvgl.canvas.draw_line: @@ -710,6 +743,9 @@ lvgl: text: format: "A string with a number %d" args: ['(int)(random_uint32() % 1000)'] + size: 120 + dark_color: navy + light_color: white - slider: min_value: 0 @@ -928,7 +964,6 @@ lvgl: grid_cell_row_pos: 0 grid_cell_column_pos: 0 src: !lambda return dog_image; - mode: virtual on_click: then: - lvgl.tabview.select: @@ -1023,10 +1058,18 @@ lvgl: text_color: 0xFFFFFF scales: - ticks: - width: !lambda return 1; + width: 1 count: 61 length: 20% + radial_offset: 5 color: 0xFFFFFF + major: + stride: 5 + width: 2 + length: 8 + color: 0xC0C0C0 + radial_offset: 3 + label_gap: 6 range_from: 0 range_to: 60 angle_range: 360 @@ -1037,15 +1080,15 @@ lvgl: end_value: 60 color_start: 0x0000bd color_end: 0xbd0000 - width: !lambda return 1; + width: 1 - line: opa: 50% id: minute_hand color: 0xFF0000 - r_mod: !lambda return -1; - width: !lambda return 3; - - - angle_range: 330 + length: 99% + radial_offset: 2 + width: 1 + - angle_range: 330 rotation: 300 range_from: 1 range_to: 12 @@ -1069,8 +1112,14 @@ lvgl: value: 180 width: 4 color: 0xA0A0A0 - r_mod: -20 + length: 80% opa: 0% + - arc: + id: meter_arc_indicator + color: 0xFF0000 + width: 6 + start_value: 0 + end_value: 360 - id: page3 layout: Horizontal pad_all: 6px diff --git a/tests/components/main.cpp b/tests/components/main.cpp index 373fde7151..622b1f107b 100644 --- a/tests/components/main.cpp +++ b/tests/components/main.cpp @@ -22,7 +22,7 @@ void original_setup() { void setup() { // Log functions call global_logger->log_vprintf_() without a null check, // so we must set up a Logger before any test that triggers logging. - static esphome::logger::Logger test_logger(0); + static esphome::logger::Logger test_logger(0, 64); test_logger.set_log_level(ESPHOME_LOG_LEVEL); test_logger.pre_setup(); diff --git a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp index 36af087d2c..5ad25c2d7d 100644 --- a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp +++ b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportBinarySensorTest, AddBinarySensor) { TestablePacketTransport transport; binary_sensor::BinarySensor bs; + transport.set_binary_sensor_count(1); transport.add_binary_sensor("motion", &bs); ASSERT_EQ(transport.binary_sensors_.size(), 1u); EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); @@ -24,6 +25,7 @@ TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) { encoder.init_for_test("sender"); binary_sensor::BinarySensor local_bs; local_bs.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("motion", &local_bs); encoder.send_data_(true); @@ -46,11 +48,13 @@ TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) { sensor::Sensor s1, s2; s1.state = 10.0f; s2.state = 20.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); binary_sensor::BinarySensor bs1; bs1.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("bs1", &bs1); encoder.send_data_(true); diff --git a/tests/components/packet_transport/sensor/sensor_test.cpp b/tests/components/packet_transport/sensor/sensor_test.cpp index 2f681aee58..5d1cfb4bc2 100644 --- a/tests/components/packet_transport/sensor/sensor_test.cpp +++ b/tests/components/packet_transport/sensor/sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportSensorTest, AddSensor) { TestablePacketTransport transport; sensor::Sensor s; + transport.set_sensor_count(1); transport.add_sensor("temp", &s); ASSERT_EQ(transport.sensors_.size(), 1u); EXPECT_STREQ(transport.sensors_[0].id, "temp"); @@ -26,6 +27,7 @@ TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) { encoder.init_for_test("sender"); sensor::Sensor local_sensor; local_sensor.state = 42.5f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -53,6 +55,7 @@ TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) { encoder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 99.9f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -77,6 +80,7 @@ TEST(PacketTransportSensorTest, SendDataOnlyUpdated) { sensor::Sensor s1, s2; s1.state = 1.0f; s2.state = 2.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); @@ -111,6 +115,7 @@ TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); // Requester sends a MAGIC_PING that the responder processes @@ -148,6 +153,7 @@ TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); responder.send_data_(true); ASSERT_EQ(responder.sent_packets.size(), 1u); diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index de01fa3602..30b99eeb70 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -8,6 +8,11 @@ on_beo4: - logger.log: format: "on_beo4: %u %u" args: ["x.source", "x.command"] +on_brennenstuhl: + then: + - logger.log: + format: "on_brennenstuhl: %u" + args: ["x.code"] on_aeha: then: - logger.log: diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index d48d36bd54..c6c7049605 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -14,6 +14,12 @@ button: remote_transmitter.transmit_beo4: source: 0x01 command: 0x0C + - platform: template + name: brennenstuhl + id: button_a_on + on_press: + remote_transmitter.transmit_brennenstuhl: + code: 0xBD2E2C - platform: template name: Dyson fan up id: dyson_fan_up diff --git a/tests/components/sht4x/common.yaml b/tests/components/sht4x/common.yaml index 50d5ad8ca4..bec192d6db 100644 --- a/tests/components/sht4x/common.yaml +++ b/tests/components/sht4x/common.yaml @@ -6,4 +6,8 @@ sensor: humidity: name: SHT4X Humidity address: 0x44 + precision: High + heater_max_duty: 0.02 + heater_power: High + heater_time: Long update_interval: 15s diff --git a/tests/components/spa06_i2c/common.yaml b/tests/components/spa06_i2c/common.yaml new file mode 100644 index 0000000000..d2be0e3ac9 --- /dev/null +++ b/tests/components/spa06_i2c/common.yaml @@ -0,0 +1,15 @@ +sensor: + - platform: spa06_i2c + i2c_id: i2c_bus + address: 0x77 + temperature: + id: spa06_i2c_temperature + name: Outside Temperature + sample_rate: 1 + oversampling: NONE + pressure: + name: Outside Pressure + id: spa06_i2c_pressure + sample_rate: 25p4 + oversampling: 16X + update_interval: 15s diff --git a/tests/components/spa06_i2c/test.esp32-idf.yaml b/tests/components/spa06_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/spa06_i2c/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_i2c/test.esp8266-ard.yaml b/tests/components/spa06_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/spa06_i2c/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_i2c/test.rp2040-ard.yaml b/tests/components/spa06_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/spa06_i2c/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index f7e2d8a3f7..de3ea3029e 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); MOCK_METHOD(size_t, available, (), (override)); - MOCK_METHOD(FlushResult, flush, (), (override)); + MOCK_METHOD(UARTFlushResult, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; diff --git a/tests/components/uart/test.rtl87xx-ard.yaml b/tests/components/uart/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..414bf1f14d --- /dev/null +++ b/tests/components/uart/test.rtl87xx-ard.yaml @@ -0,0 +1,14 @@ +uart: + - id: uart_id + tx_pin: PA23 + rx_pin: PA18 + baud_rate: 9600 + data_bits: 8 + parity: NONE + stop_bits: 1 + +switch: + - platform: uart + name: "UART Switch" + uart_id: uart_id + data: [0x01, 0x02, 0x03] diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 6fa0c08aa3..329286e2fa 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -15,7 +15,7 @@ void setup() { static char name[] = "livingroom"; static char friendly_name[] = "LivingRoom"; App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1); - auto *log = new logger::Logger(115200); // NOLINT + auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); App.register_component_(log); diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index 1a15da76d1..d0690e7515 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -153,9 +153,9 @@ bool MockUartComponent::read_array(uint8_t *data, size_t len) { size_t MockUartComponent::available() { return this->rx_buffer_.size(); } -uart::FlushResult MockUartComponent::flush() { +uart::UARTFlushResult MockUartComponent::flush() { // Nothing to flush in mock - return uart::FlushResult::ASSUMED_SUCCESS; + return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; } void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) { diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 82e3b3d563..0b3b49893d 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -28,7 +28,7 @@ class MockUartComponent : public uart::UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; size_t available() override; - uart::FlushResult flush() override; + uart::UARTFlushResult flush() override; void set_rx_full_threshold(size_t rx_full_threshold) override; void set_rx_timeout(size_t rx_timeout) override; diff --git a/tests/integration/fixtures/light_constant_brightness.yaml b/tests/integration/fixtures/light_constant_brightness.yaml new file mode 100644 index 0000000000..4357a16d58 --- /dev/null +++ b/tests/integration/fixtures/light_constant_brightness.yaml @@ -0,0 +1,57 @@ +esphome: + name: light-cb-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: cb_cold_white_output + type: float + write_action: + - logger.log: + format: "CB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: cb_warm_white_output + type: float + write_action: + - logger.log: + format: "CB_WW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_cold_white_output + type: float + write_action: + - logger.log: + format: "NCB_CW_OUTPUT:%.6f" + args: [state] + - platform: template + id: ncb_warm_white_output + type: float + write_action: + - logger.log: + format: "NCB_WW_OUTPUT:%.6f" + args: [state] + +light: + - platform: cwww + name: "Test CB Light" + id: test_cb_light + cold_white: cb_cold_white_output + warm_white: cb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: true + gamma_correct: 2.8 + + - platform: cwww + name: "Test NCB Light" + id: test_ncb_light + cold_white: ncb_cold_white_output + warm_white: ncb_warm_white_output + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: false + gamma_correct: 2.8 diff --git a/tests/integration/fixtures/uart_mock_ld2410.yaml b/tests/integration/fixtures/uart_mock_ld2410.yaml index 59838b0599..e7568c0e3f 100644 --- a/tests/integration/fixtures/uart_mock_ld2410.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml index 4625ae8511..5e62a4b8a8 100644 --- a/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2410_engineering.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412.yaml b/tests/integration/fixtures/uart_mock_ld2412.yaml index 9cf9d6bb87..525ab0d5c4 100644 --- a/tests/integration/fixtures/uart_mock_ld2412.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml index a69e18888e..9f83e3226f 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml index c0bd514762..fd1cd9fd33 100644 --- a/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml +++ b/tests/integration/fixtures/uart_mock_ld2412_engineering_truncated.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2420.yaml b/tests/integration/fixtures/uart_mock_ld2420.yaml index 5380b81071..ee22f807d4 100644 --- a/tests/integration/fixtures/uart_mock_ld2420.yaml +++ b/tests/integration/fixtures/uart_mock_ld2420.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml index 2ceca5d35d..d3b6ad5d92 100644 --- a/tests/integration/fixtures/uart_mock_ld2420_simple.yaml +++ b/tests/integration/fixtures/uart_mock_ld2420_simple.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/fixtures/uart_mock_ld2450.yaml b/tests/integration/fixtures/uart_mock_ld2450.yaml index 269136da68..4140ff659c 100644 --- a/tests/integration/fixtures/uart_mock_ld2450.yaml +++ b/tests/integration/fixtures/uart_mock_ld2450.yaml @@ -3,6 +3,7 @@ esphome: host: api: + batch_delay: 0ms # Disable batching to receive all state updates logger: level: VERBOSE diff --git a/tests/integration/test_light_constant_brightness.py b/tests/integration/test_light_constant_brightness.py new file mode 100644 index 0000000000..622dc0e065 --- /dev/null +++ b/tests/integration/test_light_constant_brightness.py @@ -0,0 +1,188 @@ +"""Integration test for constant_brightness with gamma correction. + +Tests both constant_brightness: true and false cwww lights with gamma +correction in a single compilation to verify: +- constant_brightness: true maintains constant total CW+WW power output +- constant_brightness: false correctly varies total power across color temps + +This is a regression test for https://github.com/esphome/esphome/issues/15040 +where the gamma LUT refactor (#14123) broke constant_brightness by applying +gamma after the balancing formula instead of before it. +""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import EntityState, LightInfo, LightState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_constant_brightness( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test constant_brightness true and false behavior with gamma correction.""" + # Track output values for both lights from log lines + cb_cw_pattern = re.compile(r"(? None: + for pattern, key in [ + (cb_cw_pattern, "cb_cw"), + (cb_ww_pattern, "cb_ww"), + (ncb_cw_pattern, "ncb_cw"), + (ncb_ww_pattern, "ncb_ww"), + ]: + match = pattern.search(line) + if match: + latest[key] = float(match.group(1)) + + loop = asyncio.get_running_loop() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + lights = [e for e in entities if isinstance(e, LightInfo)] + cb_light = next(e for e in lights if e.object_id.endswith("cb_light")) + ncb_light = next(e for e in lights if e.object_id.endswith("ncb_light")) + + # Use InitialStateHelper to wait for initial state broadcast + initial_state_helper = InitialStateHelper(entities) + + # Track state changes per light key + state_futures: dict[int, asyncio.Future[EntityState]] = {} + + def on_state(state: EntityState) -> None: + if isinstance(state, LightState) and state.key in state_futures: + future = state_futures[state.key] + if not future.done(): + future.set_result(state) + + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + async def send_and_wait( + light_key: int, timeout: float = 5.0, **kwargs: Any + ) -> LightState: + """Send a light command and wait for the state response.""" + state_futures[light_key] = loop.create_future() + client.light_command(key=light_key, **kwargs) + try: + return await asyncio.wait_for(state_futures[light_key], timeout=timeout) + except TimeoutError: + pytest.fail(f"Timeout waiting for light state after command: {kwargs}") + + # --- Test constant_brightness: true --- + + # Turn on CB light at full brightness + await send_and_wait( + cb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + test_mireds = [ + 153.0, # Pure cold white + 200.0, # Mostly cold + 280.0, # Mixed + 326.5, # Midpoint + 400.0, # Mostly warm + 500.0, # Pure warm white + ] + + cb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + cb_light.key, color_temperature=mireds, transition_length=0 + ) + cb_totals.append((mireds, latest["cb_cw"], latest["cb_ww"])) + + # All totals should be approximately equal (constant brightness) + reference_total = next((cw + ww for _, cw, ww in cb_totals if cw + ww > 0), 0) + assert reference_total > 0, ( + f"Reference total power is zero, CB light outputs not working. " + f"Values: {cb_totals}" + ) + + for mireds, cw, ww in cb_totals: + total = cw + ww + assert total == pytest.approx(reference_total, rel=0.05), ( + f"constant_brightness: Total power at {mireds} mireds " + f"({total:.4f}) differs from reference ({reference_total:.4f}) " + f"by more than 5%. CW={cw:.4f}, WW={ww:.4f}. " + f"All values: {cb_totals}" + ) + + # --- Test constant_brightness: false --- + + # Turn on NCB light at full brightness + await send_and_wait( + ncb_light.key, + state=True, + brightness=1.0, + color_temperature=153.0, + transition_length=0, + ) + + ncb_totals: list[tuple[float, float, float]] = [] + for mireds in test_mireds: + await send_and_wait( + ncb_light.key, color_temperature=mireds, transition_length=0 + ) + ncb_totals.append((mireds, latest["ncb_cw"], latest["ncb_ww"])) + + extreme_cw = ncb_totals[0] # 153 mireds - pure cold + extreme_ww = ncb_totals[-1] # 500 mireds - pure warm + midpoint = ncb_totals[3] # 326.5 mireds - midpoint + + # At pure cold white, WW should be ~0 + assert extreme_cw[2] == pytest.approx(0.0, abs=0.01), ( + f"Pure cold white should have WW~0, got WW={extreme_cw[2]:.4f}" + ) + # At pure warm white, CW should be ~0 + assert extreme_ww[1] == pytest.approx(0.0, abs=0.01), ( + f"Pure warm white should have CW~0, got CW={extreme_ww[1]:.4f}" + ) + + # At midpoint, both channels should be non-zero + assert midpoint[1] > 0.05, f"Midpoint CW should be >0.05, got {midpoint[1]:.4f}" + assert midpoint[2] > 0.05, f"Midpoint WW should be >0.05, got {midpoint[2]:.4f}" + + # Total power at midpoint should be higher than at the extremes + midpoint_total = midpoint[1] + midpoint[2] + extreme_cw_total = extreme_cw[1] + extreme_cw[2] + extreme_ww_total = extreme_ww[1] + extreme_ww[2] + + assert midpoint_total > extreme_cw_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure CW total " + f"({extreme_cw_total:.4f}). All values: {ncb_totals}" + ) + assert midpoint_total > extreme_ww_total, ( + f"Midpoint total ({midpoint_total:.4f}) should be > pure WW total " + f"({extreme_ww_total:.4f}). All values: {ncb_totals}" + ) diff --git a/tests/integration/test_uart_mock_ld2412.py b/tests/integration/test_uart_mock_ld2412.py index 9b928ef14f..12aa3f8397 100644 --- a/tests/integration/test_uart_mock_ld2412.py +++ b/tests/integration/test_uart_mock_ld2412.py @@ -79,9 +79,15 @@ async def test_uart_mock_ld2412( ], ) - # Signal when we see recovery frame values + # Signal when we see all recovery frame values recovery_received = collector.add_waiter( - lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + lambda: ( + pytest.approx(50.0) in collector.sensor_states["moving_distance"] + and pytest.approx(75.0) in collector.sensor_states["still_distance"] + and pytest.approx(100.0) in collector.sensor_states["moving_energy"] + and pytest.approx(80.0) in collector.sensor_states["still_energy"] + and pytest.approx(50.0) in collector.sensor_states["detection_distance"] + ) ) async with ( @@ -150,23 +156,12 @@ async def test_uart_mock_ld2412( ) # Recovery frame: moving=50, still=75, energy=100/80, detect=50 - recovery_idx = next( - i - for i, v in enumerate(collector.sensor_states["moving_distance"]) - if v == pytest.approx(50.0) - ) - assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx( - 75.0 - ) - assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx( - 100.0 - ) - assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx( - 80.0 - ) - assert collector.sensor_states["detection_distance"][ - recovery_idx - ] == pytest.approx(50.0) + # Check values exist (waiter already ensured all are present) + assert pytest.approx(50.0) in collector.sensor_states["moving_distance"] + assert pytest.approx(75.0) in collector.sensor_states["still_distance"] + assert pytest.approx(100.0) in collector.sensor_states["moving_energy"] + assert pytest.approx(80.0) in collector.sensor_states["still_energy"] + assert pytest.approx(50.0) in collector.sensor_states["detection_distance"] # Verify binary sensors detected targets (from Phase 1 frame) assert collector.binary_states["has_target"][0] is True diff --git a/tests/unit_tests/analyze_memory/test_pstorage_attribution.py b/tests/unit_tests/analyze_memory/test_pstorage_attribution.py new file mode 100644 index 0000000000..a57b283f44 --- /dev/null +++ b/tests/unit_tests/analyze_memory/test_pstorage_attribution.py @@ -0,0 +1,90 @@ +"""Tests for __pstorage symbol attribution in memory analyzer.""" + +from unittest.mock import patch + +from esphome.analyze_memory import _PSTORAGE_SUFFIX, MemoryAnalyzer + + +def _make_analyzer(external_components: set[str] | None = None) -> MemoryAnalyzer: + """Create a MemoryAnalyzer with mocked dependencies.""" + with patch.object(MemoryAnalyzer, "__init__", lambda self, *a, **kw: None): + analyzer = MemoryAnalyzer.__new__(MemoryAnalyzer) + analyzer.external_components = external_components or set() + return analyzer + + +def test_pstorage_suffix_constant() -> None: + """Verify the suffix constant matches what codegen produces.""" + assert _PSTORAGE_SUFFIX == "__pstorage" + + +def test_match_pstorage_simple_component() -> None: + """Simple component name like 'logger'.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("logger__logger_id__pstorage") + assert result == "[esphome]logger" + + +def test_match_pstorage_underscore_component() -> None: + """Component with underscore like 'web_server'.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("web_server__webserver_id__pstorage") + assert result == "[esphome]web_server" + + +def test_match_pstorage_api() -> None: + """API component.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("api__apiserver_id__pstorage") + assert result == "[esphome]api" + + +def test_match_pstorage_deep_sleep() -> None: + """Component with underscore: deep_sleep.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("deep_sleep__deepsleep__pstorage") + assert result == "[esphome]deep_sleep" + + +def test_match_pstorage_status_led() -> None: + """Component with underscore: status_led.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("status_led__statusled_id__pstorage") + assert result == "[esphome]status_led" + + +def test_match_pstorage_external_component() -> None: + """External component should be attributed correctly.""" + analyzer = _make_analyzer(external_components={"my_custom"}) + result = analyzer._match_pstorage_component("my_custom__thing_id__pstorage") + assert result == "[external]my_custom" + + +def test_match_pstorage_no_dunder_returns_none() -> None: + """Symbol without double underscore separator returns None.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("something__pstorage") + assert result is None + + +def test_match_pstorage_unknown_component_returns_none() -> None: + """Unknown component namespace returns None.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("nonexistent__thing_id__pstorage") + assert result is None + + +def test_match_pstorage_esphome_component() -> None: + """esphome:: namespace types map to the esphome component.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component( + "esphome__esphomeotacomponent_id__pstorage" + ) + assert result == "[esphome]esphome" + + +def test_match_pstorage_user_id_with_component_prefix() -> None: + """User-chosen ID that happens to contain a component name.""" + analyzer = _make_analyzer() + result = analyzer._match_pstorage_component("logger__relay1__pstorage") + assert result == "[esphome]logger" diff --git a/tests/unit_tests/components/light/__init__.py b/tests/unit_tests/components/light/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/components/light/test_gamma_table.py b/tests/unit_tests/components/light/test_gamma_table.py new file mode 100644 index 0000000000..a302a355dc --- /dev/null +++ b/tests/unit_tests/components/light/test_gamma_table.py @@ -0,0 +1,117 @@ +"""Tests for the gamma LUT table generation.""" + +import pytest + +from esphome.components.light import generate_gamma_table + + +def _simulate_gamma_correct_lut(table: list[int], value: float) -> float: + """Simulate the C++ gamma_correct_lut interpolation from light_state.cpp.""" + if value <= 0.0: + return 0.0 + if value >= 1.0: + return 1.0 + scaled = value * 255.0 + idx = int(scaled) + if idx >= 255: + return table[255] / 65535.0 + frac = scaled - idx + a = float(table[idx]) + b = float(table[idx + 1]) + return (a + frac * (b - a)) / 65535.0 + + +def test_table_length() -> None: + """Table must always have exactly 256 entries.""" + table = generate_gamma_table(2.8) + assert len(table) == 256 + + +def test_index_zero_is_zero() -> None: + """Index 0 must be 0 so true off remains off.""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[0] == 0, f"gamma={gamma}" + + +def test_index_255_is_max() -> None: + """Index 255 must be 65535 (full on).""" + for gamma in (1.0, 2.0, 2.2, 2.8, 3.0): + table = generate_gamma_table(gamma) + assert table[255] == 65535, f"gamma={gamma}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_nonzero_indices_are_nonzero(gamma: float) -> None: + """All indices > 0 must produce non-zero values. + + This prevents zero_means_zero breakage: non-zero input must always + produce non-zero output so FloatOutput applies min_power scaling. + """ + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= 1, f"gamma={gamma}, index {i}: got {table[i]}" + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_table_monotonically_nondecreasing(gamma: float) -> None: + """The gamma table must be monotonically non-decreasing.""" + table = generate_gamma_table(gamma) + for i in range(1, 256): + assert table[i] >= table[i - 1], ( + f"gamma={gamma}: table[{i}]={table[i]} < table[{i - 1}]={table[i - 1]}" + ) + + +def test_linear_gamma() -> None: + """With gamma=0 (linear), table should be evenly spaced.""" + table = generate_gamma_table(0) + assert table[0] == 0 + assert table[128] == round(128 / 255.0 * 65535) + assert table[255] == 65535 + + +@pytest.mark.parametrize("brightness", [0.01, 0.005, 0.001, 1 / 255]) +def test_small_brightness_nonzero_after_lut(brightness: float) -> None: + """Small but non-zero brightness must produce non-zero output through the LUT. + + Regression test for #15055: with zero_means_zero=true, a gamma-corrected + value of exactly 0.0 causes FloatOutput to skip min_power scaling, turning + the LED off instead of to minimum brightness. + """ + table = generate_gamma_table(2.8) + result = _simulate_gamma_correct_lut(table, brightness) + assert result > 0.0, ( + f"brightness={brightness}: gamma LUT returned 0.0, would break zero_means_zero" + ) + + +@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +def test_small_brightness_nonzero_all_gammas(gamma: float) -> None: + """1% brightness must be non-zero for all common gamma values.""" + table = generate_gamma_table(gamma) + result = _simulate_gamma_correct_lut(table, 0.01) + assert result > 0.0, f"gamma={gamma}: 1% brightness returned 0.0" + + +def test_lut_zero_returns_zero() -> None: + """LUT with input 0.0 must return 0.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 0.0) == 0.0 + + +def test_lut_one_returns_one() -> None: + """LUT with input 1.0 must return 1.0.""" + table = generate_gamma_table(2.8) + assert _simulate_gamma_correct_lut(table, 1.0) == 1.0 + + +def test_lut_output_monotonically_nondecreasing() -> None: + """LUT output must be monotonically non-decreasing across the full range.""" + table = generate_gamma_table(2.8) + prev = 0.0 + for i in range(1001): + value = i / 1000.0 + result = _simulate_gamma_correct_lut(table, value) + assert result >= prev, f"value={value}: result {result} < previous {prev}" + prev = result diff --git a/tests/unit_tests/components/test_rp2040_generate_boards.py b/tests/unit_tests/components/test_rp2040_generate_boards.py index 2e40ed08ba..551e88f6f6 100644 --- a/tests/unit_tests/components/test_rp2040_generate_boards.py +++ b/tests/unit_tests/components/test_rp2040_generate_boards.py @@ -59,6 +59,7 @@ def _add_board( vendor: str = "", name: str | None = None, pins_header: str | None = None, + extra_flags: str = "", ) -> None: """Add a board JSON and variant to the fake arduino-pico tree.""" if variant is None: @@ -69,11 +70,15 @@ def _add_board( json_dir = arduino_pico / "tools" / "json" variants_dir = arduino_pico / "variants" + build: dict = { + "mcu": mcu, + "variant": variant, + } + if extra_flags: + build["extra_flags"] = extra_flags + board_json = { - "build": { - "mcu": mcu, - "variant": variant, - }, + "build": build, "name": name, "vendor": vendor, } @@ -271,3 +276,35 @@ def test_placeholder_pins_not_treated_as_virtual(arduino_pico: Path) -> None: assert "MISO" not in board_pins["badpin"] assert boards["badpin"]["max_virtual_pin"] == 64 + + +def test_cyw43_supported_flag_sets_wifi(arduino_pico: Path) -> None: + """Boards with PICO_CYW43_SUPPORTED=1 in extra_flags should have wifi=True.""" + _add_board( + arduino_pico, + "rpipicow", + vendor="Raspberry Pi", + name="Pico W", + pins_header=PICOW_PINS_HEADER, + extra_flags="-DARDUINO_RASPBERRY_PI_PICO_W -DPICO_CYW43_SUPPORTED=1 -DCYW43_PIN_WL_DYNAMIC=1", + ) + + _, boards = load_boards(arduino_pico) + + assert boards["rpipicow"]["wifi"] is True + + +def test_board_without_cyw43_has_no_wifi(arduino_pico: Path) -> None: + """Boards without PICO_CYW43_SUPPORTED should not have wifi field.""" + _add_board( + arduino_pico, + "rpipico", + vendor="Raspberry Pi", + name="Pico", + pins_header=PICO_PINS_HEADER, + extra_flags="-DARDUINO_RASPBERRY_PI_PICO", + ) + + _, boards = load_boards(arduino_pico) + + assert "wifi" not in boards["rpipico"] diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py index 3f32a117ff..8d01fef7c2 100644 --- a/tests/unit_tests/test_codegen.py +++ b/tests/unit_tests/test_codegen.py @@ -1,6 +1,7 @@ import pytest from esphome import codegen as cg +from esphome.cpp_generator import _extract_component_ns # Test interface remains the same. @@ -75,3 +76,29 @@ from esphome import codegen as cg ) def test_exists(attr): assert hasattr(cg, attr) + + +@pytest.mark.parametrize( + ("type_str", "expected"), + ( + ("esphome::dsmr::Dsmr", "dsmr"), + ("esphome::logger::Logger", "logger"), + ("esphome::web_server::WebServer", "web_server"), + ("esphome::deep_sleep::DeepSleep", "deep_sleep"), + ("esphome::Component", "esphome"), + ("Logger", "esphome"), + # Template types with :: in template args must not confuse extraction + ( + "esphome::Automation, std::optional>", + "esphome", + ), + ( + "esphome::StatelessLambdaAction, std::optional>", + "esphome", + ), + # Namespaced template type + ("esphome::sensor::Sensor", "sensor"), + ), +) +def test_extract_component_ns(type_str, expected): + assert _extract_component_ns(type_str) == expected diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index c8cb3e144f..adb7658bfd 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -6,6 +6,7 @@ import pytest from esphome import core, yaml_util from esphome.components import substitutions +from esphome.config_helpers import Extend, Remove from esphome.core import EsphomeError from esphome.util import OrderedDict @@ -306,3 +307,128 @@ def test_dump_sort_keys() -> None: # nested keys should also be sorted assert "a_key:" in sorted_dump assert sorted_dump.index("a_key:") < sorted_dump.index("z_key:") + + +@pytest.mark.parametrize( + "data", + [ + { + "key1": "value1", + "key2": 42, + }, + [1, 2, 3], + "simple string", + ], +) +def test_config_context_mixin(data) -> None: + """Test that ConfigContext mixin correctly stores and retrieves context vars in a dict.""" + + context_vars = { + "var1": "context_value1", + "var2": 100, + } + + # Add context to the data + tagged_data = yaml_util.add_context(data, context_vars) + + # Check that tagged_data has ConfigContext and correct vars + assert isinstance(tagged_data, type(data)) + assert isinstance(tagged_data, yaml_util.ConfigContext) + assert tagged_data.vars == context_vars + + # Check that original data is preserved + assert tagged_data == data + + +def test_config_context_mixin_no_context() -> None: + """Test that add_context does not tag data when no context vars are provided.""" + data = {"key": "value"} + + # Add context with None + tagged_data = yaml_util.add_context(data, None) + + # Should return original data without tagging + assert tagged_data is data + assert not isinstance(tagged_data, yaml_util.ConfigContext) + + +def test_config_context_mixin_merge_contexts() -> None: + """Test that add_context merges new context vars with existing ones.""" + data = {"key": "value"} + + initial_context = { + "var1": "initial_value", + } + + # First, add initial context + tagged_data = yaml_util.add_context(data, initial_context) + + assert isinstance(tagged_data, yaml_util.ConfigContext) + assert tagged_data.vars == initial_context + + # Now, add more context vars + new_context = { + "var2": "new_value", + "var1": "overridden_value", # This should override the initial var1 + } + + merged_tagged_data = yaml_util.add_context(tagged_data, new_context) + + # Check that merged_tagged_data has merged context vars + expected_context = { + "var1": "overridden_value", + "var2": "new_value", + } + assert isinstance(merged_tagged_data, yaml_util.ConfigContext) + assert merged_tagged_data.vars == expected_context + + # Check that original data is preserved + assert merged_tagged_data == data + + +@pytest.mark.parametrize("data", [42, 3.14, True, None]) +def test_config_context_non_taggable(data) -> None: + """Test that add_context ignores non-string scalar values.""" + + context_vars = { + "var1": "context_value", + } + + # Add context to the scalar data + tagged_data = yaml_util.add_context(data, context_vars) + + # Check that tagged_data has ConfigContext and correct vars + assert not isinstance(tagged_data, yaml_util.ConfigContext) + + # Check that original data is preserved + assert tagged_data == data + + +def test_config_context_defaults_only() -> None: + """Test that defaults: key is popped and used as context vars when no explicit vars given.""" + data = {"defaults": {"x": "1", "y": "2"}, "key": "value"} + tagged = yaml_util.add_context(data, None) + + assert isinstance(tagged, yaml_util.ConfigContext) + assert tagged.vars == {"x": "1", "y": "2"} + assert "defaults" not in tagged + + +def test_config_context_defaults_explicit_vars_override() -> None: + """Test that explicit vars take precedence over defaults: values.""" + data = {"defaults": {"x": "default_x", "z": "default_z"}, "key": "value"} + tagged = yaml_util.add_context(data, {"x": "explicit_x", "w": "explicit_w"}) + + assert isinstance(tagged, yaml_util.ConfigContext) + assert tagged.vars == {"x": "explicit_x", "z": "default_z", "w": "explicit_w"} + assert "defaults" not in tagged + + +def test_represent_extend() -> None: + """Test that Extend objects are dumped as plain !extend scalars.""" + assert yaml_util.dump({"key": Extend("my_id")}) == "key: !extend 'my_id'\n" + + +def test_represent_remove() -> None: + """Test that Remove objects are dumped as plain !remove scalars.""" + assert yaml_util.dump({"key": Remove("my_id")}) == "key: !remove 'my_id'\n"