diff --git a/.claude/skills/pr-workflow/SKILL.md b/.claude/skills/pr-workflow/SKILL.md new file mode 100644 index 0000000000..4ec2551804 --- /dev/null +++ b/.claude/skills/pr-workflow/SKILL.md @@ -0,0 +1,96 @@ +--- +name: pr-workflow +description: Create pull requests for esphome. Use when creating PRs, submitting changes, or preparing contributions. +allowed-tools: Read, Bash, Glob, Grep +--- + +# ESPHome PR Workflow + +When creating a pull request for esphome, follow these steps: + +## 1. Create Branch from Upstream + +Always base your branch on **upstream** (not origin/fork) to ensure you have the latest code: + +```bash +git fetch upstream +git checkout -b upstream/dev +``` + +## 2. Read the PR Template + +Before creating a PR, read `.github/PULL_REQUEST_TEMPLATE.md` to understand required fields. + +## 3. Create the PR + +Use `gh pr create` with the **full template** filled in. Never skip or abbreviate sections. + +Required fields: +- **What does this implement/fix?**: Brief description of changes +- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.) +- **Related issue**: Use `fixes ` syntax if applicable +- **Pull request in esphome-docs**: Link if docs are needed +- **Test Environment**: Check platforms you tested on +- **Example config.yaml**: Include working example YAML +- **Checklist**: Verify code is tested and tests added + +## 4. Example PR Body + +```markdown +# What does this implement/fix? + + + +## Types of changes + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [x] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Developer breaking change (an API change that could break external components) +- [ ] Code quality improvements to existing code or addition of tests +- [ ] Other + +**Related issue or feature (if applicable):** + +- fixes https://github.com/esphome/esphome/issues/XXX + +**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** + +- esphome/esphome-docs#XXX + +## Test Environment + +- [x] ESP32 +- [x] ESP32 IDF +- [ ] ESP8266 +- [ ] RP2040 +- [ ] BK72xx +- [ ] RTL87xx +- [ ] LN882x +- [ ] nRF52840 + +## Example entry for `config.yaml`: + +```yaml +# Example config.yaml +component_name: + id: my_component + option: value +``` + +## Checklist: + - [x] The code change is tested and works locally. + - [x] Tests have been added to verify that the new code works (under `tests/` folder). + +If user exposed functionality or configuration variables are added/changed: + - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). +``` + +## 5. Push and Create PR + +```bash +git push -u origin +gh pr create --repo esphome/esphome --base dev --title "[component] Brief description" +``` + +Title should be prefixed with the component name in brackets, e.g. `[safe_mode] Add feature`. diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 75586fd854..370c8bcc46 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: venv # yamllint disable-line rule:line-length diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 434aa388f7..81d3c826d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: venv # yamllint disable-line rule:line-length @@ -157,7 +157,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: venv key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -193,7 +193,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Restore components graph cache - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -223,7 +223,7 @@ jobs: echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT - name: Save components graph cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -245,7 +245,7 @@ jobs: python-version: "3.13" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -334,14 +334,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} @@ -413,14 +413,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -502,14 +502,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -735,7 +735,7 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis if: steps.check-script.outputs.skip != 'true' - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -759,7 +759,7 @@ jobs: - name: Cache platformio if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} @@ -800,7 +800,7 @@ jobs: - name: Save memory analysis to cache if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' - uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -847,7 +847,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3295cf070a..06f9bf2a5b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.11 + rev: v0.14.13 hooks: # Run the linter. - id: ruff diff --git a/esphome/__main__.py b/esphome/__main__.py index 3849a585ca..545464be10 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -222,8 +222,13 @@ def choose_upload_log_host( else: resolved.append(device) if not resolved: + if CORE.dashboard: + hint = "If you know the IP, set 'use_address' in your network config." + else: + hint = "If you know the IP, try --device " raise EsphomeError( - f"All specified devices {defaults} could not be resolved. Is the device connected to the network?" + f"All specified devices {defaults} could not be resolved. " + f"Is the device connected to the network? {hint}" ) return resolved diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 9c935c78fa..63ef0e74ed 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -22,7 +22,7 @@ from .helpers import ( map_section_name, parse_symbol_line, ) -from .toolchain import find_tool, run_tool +from .toolchain import find_tool, resolve_tool_path, run_tool if TYPE_CHECKING: from esphome.platformio_api import IDEData @@ -132,6 +132,12 @@ class MemoryAnalyzer: readelf_path = readelf_path or idedata.readelf_path _LOGGER.debug("Using toolchain paths from PlatformIO idedata") + # Validate paths exist, fall back to find_tool if they don't + # This handles cases like Zephyr where cc_path doesn't include full path + # and the toolchain prefix may differ (e.g., arm-zephyr-eabi- vs arm-none-eabi-) + objdump_path = resolve_tool_path("objdump", objdump_path, objdump_path) + readelf_path = resolve_tool_path("readelf", readelf_path, objdump_path) + self.objdump_path = objdump_path or "objdump" self.readelf_path = readelf_path or "readelf" self.external_components = external_components or set() diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 9933bd77fd..83547b1eb5 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -9,11 +9,61 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::") # Maps standard section names to their various platform-specific variants # Note: Order matters! More specific patterns (.bss) must come before general ones (.dram) # because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise +# +# Platform-specific sections: +# - ESP8266/ESP32: .iram*, .dram* +# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM) +# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors) +# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code) +# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots) SECTION_MAPPING = { - ".text": frozenset([".text", ".iram"]), - ".rodata": frozenset([".rodata"]), - ".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss" - ".data": frozenset([".data", ".dram"]), + ".text": frozenset( + [ + ".text", + ".iram", + # LibreTiny RTL87xx XIP (eXecute In Place) flash code + ".xip.code", + # LibreTiny RTL87xx RAM code + ".ram.code_text", + # LibreTiny BK7231 fast RAM code and vectors + ".itcm.code", + ".vectors", + # LibreTiny LN882X flash code + ".flash_text", + ".flash_copy", + # Zephyr/nRF52 sections (no leading dots) + "text", + "rom_start", + ] + ), + ".rodata": frozenset( + [ + ".rodata", + # LibreTiny RTL87xx read-only data in RAM + ".ram.code_rodata", + # Zephyr/nRF52 sections (no leading dots) + "rodata", + ] + ), + # .bss patterns - must be before .data to catch ".dram0.bss" + ".bss": frozenset( + [ + ".bss", + # LibreTiny LN882X BSS + ".bss_ram", + # Zephyr/nRF52 sections (no leading dots) + "bss", + "noinit", + ] + ), + ".data": frozenset( + [ + ".data", + ".dram", + # Zephyr/nRF52 sections (no leading dots) + "datas", + ] + ), } # Section to ComponentMemory attribute mapping diff --git a/esphome/analyze_memory/helpers.py b/esphome/analyze_memory/helpers.py index cb503b37c5..a6ca7e7f0d 100644 --- a/esphome/analyze_memory/helpers.py +++ b/esphome/analyze_memory/helpers.py @@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None: return None # Find section, size, and name + # Try each part as a potential section name for i, part in enumerate(parts): - if not part.startswith("."): - continue - + # Skip parts that are clearly flags, addresses, or other metadata + # Sections start with '.' (standard ELF) or are known section names (Zephyr) section = map_section_name(part) if not section: - break + continue # Need at least size field after section if i + 1 >= len(parts): diff --git a/esphome/analyze_memory/toolchain.py b/esphome/analyze_memory/toolchain.py index 23d85e9700..3a8a5f7be4 100644 --- a/esphome/analyze_memory/toolchain.py +++ b/esphome/analyze_memory/toolchain.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import os from pathlib import Path import subprocess from typing import TYPE_CHECKING @@ -17,10 +18,82 @@ TOOLCHAIN_PREFIXES = [ "xtensa-lx106-elf-", # ESP8266 "xtensa-esp32-elf-", # ESP32 "xtensa-esp-elf-", # ESP32 (newer IDF) + "arm-zephyr-eabi-", # nRF52/Zephyr SDK + "arm-none-eabi-", # Generic ARM (RP2040, etc.) "", # System default (no prefix) ] +def _find_in_platformio_packages(tool_name: str) -> str | None: + """Search for a tool in PlatformIO package directories. + + This handles cases like Zephyr SDK where tools are installed in nested + directories that aren't in PATH. + + Args: + tool_name: Name of the tool (e.g., "readelf", "objdump") + + Returns: + Full path to the tool or None if not found + """ + # Get PlatformIO packages directory + platformio_home = Path(os.path.expanduser("~/.platformio/packages")) + if not platformio_home.exists(): + return None + + # Search patterns for toolchains that might contain the tool + # Order matters - more specific patterns first + search_patterns = [ + # Zephyr SDK deeply nested structure (4 levels) + # e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump + f"toolchain-*/*/*/bin/*-{tool_name}", + # Zephyr SDK nested structure (3 levels) + f"toolchain-*/*/bin/*-{tool_name}", + f"toolchain-*/bin/*-{tool_name}", + # Standard PlatformIO toolchain structure + f"toolchain-*/bin/*{tool_name}", + ] + + for pattern in search_patterns: + matches = list(platformio_home.glob(pattern)) + if matches: + # Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi + matches.sort(key=lambda p: ("zephyr" not in str(p), str(p))) + tool_path = str(matches[0]) + _LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path) + return tool_path + + return None + + +def resolve_tool_path( + tool_name: str, + derived_path: str | None, + objdump_path: str | None = None, +) -> str | None: + """Resolve a tool path, falling back to find_tool if derived path doesn't exist. + + Args: + tool_name: Name of the tool (e.g., "objdump", "readelf") + derived_path: Path derived from idedata (may not exist for some platforms) + objdump_path: Path to objdump binary to derive other tool paths from + + Returns: + Resolved path to the tool, or the original derived_path if it exists + """ + if derived_path and not Path(derived_path).exists(): + found = find_tool(tool_name, objdump_path) + if found: + _LOGGER.debug( + "Derived %s path %s not found, using %s", + tool_name, + derived_path, + found, + ) + return found + return derived_path + + def find_tool( tool_name: str, objdump_path: str | None = None, @@ -28,7 +101,8 @@ def find_tool( """Find a toolchain tool by name. First tries to derive the tool path from objdump_path (if provided), - then falls back to searching for platform-specific tools. + then searches PlatformIO package directories (for cross-compile toolchains), + and finally falls back to searching for platform-specific tools in PATH. Args: tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt") @@ -47,7 +121,13 @@ def find_tool( _LOGGER.debug("Found %s at: %s", tool_name, potential_path) return potential_path - # Try platform-specific tools + # Search in PlatformIO packages directory first (handles Zephyr SDK, etc.) + # This must come before PATH search because system tools (e.g., /usr/bin/objdump) + # are for the host architecture, not the target (ARM, Xtensa, etc.) + if found := _find_in_platformio_packages(tool_name): + return found + + # Try platform-specific tools in PATH (fallback for when tools are installed globally) for prefix in TOOLCHAIN_PREFIXES: cmd = f"{prefix}{tool_name}" try: diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 89c0908a74..248b5065ad 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -31,7 +31,8 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { this->last_update_ = millis(); if (state != this->current_state_) { auto prev_state = this->current_state_; - ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), + ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), + LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; // Single state callback - triggers check get_state() for specific states diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a4eeb4dd5e..ed97c3b9a2 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -241,8 +241,10 @@ void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ if (obj->is_internal()) \ return; \ - for (auto &c : this->clients_) \ - c->send_##entity_name##_state(obj); \ + for (auto &c : this->clients_) { \ + if (c->flags_.state_subscription) \ + c->send_##entity_name##_state(obj); \ + } \ } #ifdef USE_BINARY_SENSOR @@ -321,8 +323,10 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater) void APIServer::on_event(event::Event *obj) { if (obj->is_internal()) return; - for (auto &c : this->clients_) - c->send_event(obj); + for (auto &c : this->clients_) { + if (c->flags_.state_subscription) + c->send_event(obj); + } } #endif @@ -331,8 +335,10 @@ void APIServer::on_event(event::Event *obj) { void APIServer::on_update(update::UpdateEntity *obj) { if (obj->is_internal()) return; - for (auto &c : this->clients_) - c->send_update_state(obj); + for (auto &c : this->clients_) { + if (c->flags_.state_subscription) + c->send_update_state(obj); + } } #endif @@ -552,8 +558,10 @@ bool APIServer::clear_noise_psk(bool make_active) { #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->flags_.remove && client->is_authenticated()) + if (!client->flags_.remove && client->is_authenticated()) { client->send_time_request(); + return; // Only request from one client to avoid clock conflicts + } } } #endif diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index eac26997cf..2a0ddf91db 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size } uint32_t field_length = res->as_uint32(); ptr += consumed; - if (ptr + field_length > end) { + if (field_length > static_cast(end - ptr)) { return count; // Out of bounds } ptr += field_length; break; } case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes - if (ptr + 4 > end) { + if (end - ptr < 4) { return count; } ptr += 4; @@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { } uint32_t field_length = res->as_uint32(); ptr += consumed; - if (ptr + field_length > end) { + if (field_length > static_cast(end - ptr)) { ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); return; } @@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { break; } case WIRE_TYPE_FIXED32: { // 32-bit - if (ptr + 4 > end) { + if (end - ptr < 4) { ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); return; } diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 86b7350aa8..4fe2a019e0 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -44,7 +44,7 @@ bool BinarySensor::set_new_state(const optional &new_state) { #if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_binary_sensor_update(this); #endif - ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); + ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); return true; } return false; diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 4b30dc5d16..049618219e 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -44,7 +44,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase ), - cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"), + cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"), } ).extend(cv.COMPONENT_SCHEMA), cv.only_on( diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 7611d33cbf..816bd5dfcb 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -436,7 +436,7 @@ void Climate::save_state_() { } void Climate::publish_state() { - ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); + ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index feac9823b9..97b8c2213e 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -153,7 +153,7 @@ void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str()); + ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index a1b684abbf..13f2fa59bd 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -44,7 +44,7 @@ void DallasTemperatureSensor::update() { this->send_command_(DALLAS_COMMAND_START_CONVERSION); - this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] { + this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] { if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) { this->publish_state(NAN); return; diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index c061bc81f7..c5ea051914 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -30,7 +30,7 @@ void DateEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); + ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); #if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_date_update(this); diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 694f9c5721..fd3901fcfc 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -45,8 +45,8 @@ void DateTimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, - this->month_, this->day_, this->hour_, this->minute_, this->second_); + ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, + this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_datetime_update(this); diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 0e71c95238..d0b8875ed1 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -26,8 +26,7 @@ void TimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, - this->second_); + ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_time_update(this); diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 5783bc5418..6cf52d890c 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -74,8 +74,11 @@ class DebugComponent : public PollingComponent { #ifdef USE_SENSOR void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + void set_min_free_sensor(sensor::Sensor *min_free_sensor) { min_free_sensor_ = min_free_sensor; } #endif void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } #ifdef USE_ESP32 @@ -97,8 +100,11 @@ class DebugComponent : public PollingComponent { sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr}; -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) sensor::Sensor *fragmentation_sensor_{nullptr}; +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + sensor::Sensor *min_free_sensor_{nullptr}; #endif sensor::Sensor *loop_time_sensor_{nullptr}; #ifdef USE_ESP32 diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index ebb6abf4da..8c41011f7d 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -234,8 +234,19 @@ size_t DebugComponent::get_device_info_(std::span void DebugComponent::update_platform_() { #ifdef USE_SENSOR + uint32_t max_alloc = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); if (this->block_sensor_ != nullptr) { - this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); + this->block_sensor_->publish_state(max_alloc); + } + if (this->min_free_sensor_ != nullptr) { + this->min_free_sensor_->publish_state(heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL)); + } + if (this->fragmentation_sensor_ != nullptr) { + uint32_t free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL); + if (free_heap > 0) { + float fragmentation = 100.0f - (100.0f * max_alloc / free_heap); + this->fragmentation_sensor_->publish_state(fragmentation); + } } if (this->psram_sensor_ != nullptr) { this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 4f07a4cc17..aae27c8ca2 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -51,6 +51,9 @@ void DebugComponent::update_platform_() { if (this->block_sensor_ != nullptr) { this->block_sensor_->publish_state(lt_heap_get_max_alloc()); } + if (this->min_free_sensor_ != nullptr) { + this->min_free_sensor_->publish_state(lt_heap_get_min_free()); + } #endif } diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index 4484f15935..0a716d666e 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -11,16 +11,24 @@ from esphome.const import ( ENTITY_CATEGORY_DIAGNOSTIC, ICON_COUNTER, ICON_TIMER, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, UNIT_BYTES, UNIT_HERTZ, UNIT_MILLISECOND, UNIT_PERCENT, ) -from . import CONF_DEBUG_ID, DebugComponent +from . import ( # noqa: F401 pylint: disable=unused-import + CONF_DEBUG_ID, + FILTER_SOURCE_FILES, + DebugComponent, +) DEPENDENCIES = ["debug"] +CONF_MIN_FREE = "min_free" CONF_PSRAM = "psram" CONFIG_SCHEMA = { @@ -38,8 +46,14 @@ CONFIG_SCHEMA = { entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_FRAGMENTATION): cv.All( - cv.only_on_esp8266, - cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), + cv.Any( + cv.All( + cv.only_on_esp8266, + cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), + ), + cv.only_on_esp32, + msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32", + ), sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, icon=ICON_COUNTER, @@ -47,6 +61,19 @@ CONFIG_SCHEMA = { entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), + cv.Optional(CONF_MIN_FREE): cv.All( + cv.Any( + cv.only_on_esp32, + cv.only_on([PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]), + msg="This feature is only available on ESP32 and LibreTiny (BK72xx, LN882x, RTL87xx)", + ), + sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MILLISECOND, icon=ICON_TIMER, @@ -89,6 +116,10 @@ async def to_code(config): sens = await sensor.new_sensor(fragmentation_conf) cg.add(debug_component.set_fragmentation_sensor(sens)) + if min_free_conf := config.get(CONF_MIN_FREE): + sens = await sensor.new_sensor(min_free_conf) + cg.add(debug_component.set_min_free_sensor(sens)) + if loop_time_conf := config.get(CONF_LOOP_TIME): sens = await sensor.new_sensor(loop_time_conf) cg.add(debug_component.set_loop_time_sensor(sens)) diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py index 96ef231850..c69b8d9461 100644 --- a/esphome/components/debug/text_sensor.py +++ b/esphome/components/debug/text_sensor.py @@ -8,7 +8,11 @@ from esphome.const import ( ICON_RESTART, ) -from . import CONF_DEBUG_ID, DebugComponent +from . import ( # noqa: F401 pylint: disable=unused-import + CONF_DEBUG_ID, + FILTER_SOURCE_FILES, + DebugComponent, +) DEPENDENCIES = ["debug"] diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 149fcc79d5..01f79156a9 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -193,10 +193,18 @@ void BLEClientBase::log_event_(const char *name) { ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); } -void BLEClientBase::log_gattc_event_(const char *name) { +void BLEClientBase::log_gattc_lifecycle_event_(const char *name) { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); } +void BLEClientBase::log_gattc_data_event_(const char *name) { + // Data transfer events are logged at VERBOSE level because logging to UART creates + // delays that cause timing issues during time-sensitive BLE operations. This is + // especially problematic during pairing or firmware updates which require rapid + // writes to many characteristics - the log spam can cause these operations to fail. + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); +} + void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) { ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status); } @@ -280,7 +288,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_OPEN_EVT: { if (!this->check_addr(param->open.remote_bda)) return false; - this->log_gattc_event_("OPEN"); + this->log_gattc_lifecycle_event_("OPEN"); // conn_id was already set in ESP_GATTC_CONNECT_EVT this->service_count_ = 0; @@ -331,7 +339,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_CONNECT_EVT: { if (!this->check_addr(param->connect.remote_bda)) return false; - this->log_gattc_event_("CONNECT"); + this->log_gattc_lifecycle_event_("CONNECT"); this->conn_id_ = param->connect.conn_id; // Start MTU negotiation immediately as recommended by ESP-IDF examples // (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in @@ -376,7 +384,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_CLOSE_EVT: { if (this->conn_id_ != param->close.conn_id) return false; - this->log_gattc_event_("CLOSE"); + this->log_gattc_lifecycle_event_("CLOSE"); this->release_services(); this->set_state(espbt::ClientState::IDLE); this->conn_id_ = UNSET_CONN_ID; @@ -404,7 +412,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_SEARCH_CMPL_EVT: { if (this->conn_id_ != param->search_cmpl.conn_id) return false; - this->log_gattc_event_("SEARCH_CMPL"); + this->log_gattc_lifecycle_event_("SEARCH_CMPL"); // For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery // This balances performance with bandwidth usage after the critical discovery phase if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { @@ -431,35 +439,35 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_READ_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("READ_DESCR"); + this->log_gattc_data_event_("READ_DESCR"); break; } case ESP_GATTC_WRITE_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("WRITE_DESCR"); + this->log_gattc_data_event_("WRITE_DESCR"); break; } case ESP_GATTC_WRITE_CHAR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_gattc_event_("WRITE_CHAR"); + this->log_gattc_data_event_("WRITE_CHAR"); break; } case ESP_GATTC_READ_CHAR_EVT: { if (this->conn_id_ != param->read.conn_id) return false; - this->log_gattc_event_("READ_CHAR"); + this->log_gattc_data_event_("READ_CHAR"); break; } case ESP_GATTC_NOTIFY_EVT: { if (this->conn_id_ != param->notify.conn_id) return false; - this->log_gattc_event_("NOTIFY"); + this->log_gattc_data_event_("NOTIFY"); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->log_gattc_event_("REG_FOR_NOTIFY"); + this->log_gattc_data_event_("REG_FOR_NOTIFY"); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // Client is responsible for flipping the descriptor value @@ -491,7 +499,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_err_t status = esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); - ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); + ESP_LOGV(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); if (status) { this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status); } @@ -499,13 +507,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { - this->log_gattc_event_("UNREG_FOR_NOTIFY"); + this->log_gattc_data_event_("UNREG_FOR_NOTIFY"); break; } default: - // ideally would check all other events for matching conn_id - ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); + // Unknown events logged at VERBOSE to avoid UART delays during time-sensitive operations + ESP_LOGV(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); break; } return true; diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 92c7444ee1..c52f0e5d2d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -127,7 +127,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { // 6 bytes used, 2 bytes padding void log_event_(const char *name); - void log_gattc_event_(const char *name); + void log_gattc_lifecycle_event_(const char *name); + void log_gattc_data_event_(const char *name); void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type); void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 9f8ae3277e..d69a438578 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -294,8 +294,7 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() { } // Stream firmware to coprocessor while computing SHA256 - // Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+) - alignas(32) sha256::SHA256 hasher; + sha256::SHA256 hasher; hasher.init(); uint8_t buffer[CHUNK_SIZE]; @@ -352,8 +351,7 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() { } // Verify SHA256 before writing - // Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+) - alignas(32) sha256::SHA256 hasher; + sha256::SHA256 hasher; hasher.init(); hasher.add(this->firmware_data_, this->firmware_size_); hasher.calculate(); diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b2ae185687..df2ea98f2c 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -563,11 +563,9 @@ bool ESPHomeOTAComponent::handle_auth_send_() { // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash - // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame + // CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame // (no passing to other functions). All hash operations must happen in this function. - // NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for - // hardware SHA acceleration DMA operations. - alignas(32) sha256::SHA256 hasher; + sha256::SHA256 hasher; const size_t hex_size = hasher.get_size() * 2; const size_t nonce_len = hasher.get_size() / 4; @@ -639,11 +637,9 @@ bool ESPHomeOTAComponent::handle_auth_read_() { const char *cnonce = nonce + hex_size; const char *response = cnonce + hex_size; - // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame + // CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame // (no passing to other functions). All hash operations must happen in this function. - // NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for - // hardware SHA acceleration DMA operations. - alignas(32) sha256::SHA256 hasher; + sha256::SHA256 hasher; hasher.init(); hasher.add(this->password_.c_str(), this->password_.length()); diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 4c74a11388..8015f2255a 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) { return; } this->last_event_type_ = found; - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_); + ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(event_type); #if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_event(this); diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 2e48d84eb9..02fde730eb 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -201,7 +201,7 @@ void Fan::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, - "'%s' - Sending state:\n" + "'%s' >>\n" " State: %s", this->name_.c_str(), ONOFF(this->state)); if (traits.supports_speed()) { diff --git a/esphome/components/hmac_sha256/hmac_sha256.cpp b/esphome/components/hmac_sha256/hmac_sha256.cpp index cf5daf63af..2146e961bc 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.cpp +++ b/esphome/components/hmac_sha256/hmac_sha256.cpp @@ -1,4 +1,3 @@ -#include #include #include "hmac_sha256.h" #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) @@ -26,9 +25,7 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_ void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); } void HmacSHA256::get_hex(char *output) { - for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) { - sprintf(output + (i * 2), "%02x", this->digest_[i]); - } + format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE); } bool HmacSHA256::equals_bytes(const uint8_t *expected) { diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 1b5fd9f00e..a8c2cdfc63 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -242,9 +242,7 @@ template class HttpRequestSendAction : public Action { return; } - size_t content_length = container->content_length; - size_t max_length = std::min(content_length, this->max_response_buffer_size_); - + size_t max_length = this->max_response_buffer_size_; #ifdef USE_HTTP_REQUEST_RESPONSE if (this->capture_response_.value(x...)) { std::string response_body; diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 725a9c1c1e..1de947ba5b 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -213,18 +213,12 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - int bufsize = std::min(max_len, this->content_length - this->bytes_read_); - - if (bufsize == 0) { - this->duration_ms += (millis() - start); - return 0; + this->feed_wdt(); + int read_len = esp_http_client_read(this->client_, (char *) buf, max_len); + this->feed_wdt(); + if (read_len > 0) { + this->bytes_read_ += read_len; } - - this->feed_wdt(); - int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize); - this->feed_wdt(); - this->bytes_read_ += read_len; - this->duration_ms += (millis() - start); return read_len; diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 20c731e730..0eeb4bba33 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -1,3 +1,4 @@ +import logging from typing import Any from esphome import automation, pins @@ -18,13 +19,16 @@ from esphome.const import ( CONF_ROTATION, CONF_UPDATE_INTERVAL, ) -from esphome.core import ID +from esphome.core import ID, EnumValue from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv +from esphome.helpers import add_class_to_obj from esphome.types import ConfigType from . import boards, hub75_ns +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ["esp32"] CODEOWNERS = ["@stuartparmenter"] @@ -120,13 +124,51 @@ PANEL_LAYOUTS = { } Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True) -SCAN_PATTERNS = { +SCAN_WIRINGS = { "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, - "FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH, - "FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH, - "FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH, + "SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH, + "SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH, + "SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH, + "SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH, } +# Deprecated scan wiring names - mapped to new names +DEPRECATED_SCAN_WIRINGS = { + "FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH", + "FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH", + "FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH", +} + + +def _validate_scan_wiring(value): + """Validate scan_wiring with deprecation warnings for old names.""" + value = cv.string(value).upper().replace(" ", "_") + + # Check if using deprecated name + # Remove deprecated names in 2026.7.0 + if value in DEPRECATED_SCAN_WIRINGS: + new_name = DEPRECATED_SCAN_WIRINGS[value] + _LOGGER.warning( + "Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. " + "Please use '%s' instead.", + value, + new_name, + ) + value = new_name + + # Validate against allowed values + if value not in SCAN_WIRINGS: + raise cv.Invalid( + f"Unknown scan wiring '{value}'. " + f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}" + ) + + # Return as EnumValue like cv.enum does + result = add_class_to_obj(value, EnumValue) + result.enum_value = SCAN_WIRINGS[value] + return result + + Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True) CLOCK_SPEEDS = { "8MHZ": Hub75ClockSpeed.HZ_8M, @@ -382,9 +424,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_LAYOUT_COLS): cv.positive_int, cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"), # Panel hardware configuration - cv.Optional(CONF_SCAN_WIRING): cv.enum( - SCAN_PATTERNS, upper=True, space="_" - ), + cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring, cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True), # Display configuration cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean, @@ -547,7 +587,7 @@ def _build_config_struct( async def to_code(config: ConfigType) -> None: add_idf_component( name="esphome/esp-hub75", - ref="0.2.2", + ref="0.3.0", ) # Set compile-time configuration via build flags (so external library sees them) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 3f8d909824..6ff75d7709 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -665,15 +665,10 @@ async def write_image(config, all_frames=False): if is_svg_file(path): import resvg_py - if resize: - width, height = resize - # resvg-py allows rendering by width/height directly - image_data = resvg_py.svg_to_bytes( - svg_path=str(path), width=int(width), height=int(height) - ) - else: - # Default size - image_data = resvg_py.svg_to_bytes(svg_path=str(path)) + resize = resize or (None, None) + image_data = resvg_py.svg_to_bytes( + svg_path=str(path), width=resize[0], height=resize[1], dpi=100 + ) # Convert bytes to Pillow Image image = Image.open(io.BytesIO(image_data)) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 5f8d63926a..4431869951 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -18,7 +18,15 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) { InfraredCall &InfraredCall::set_raw_timings(const std::vector &timings) { this->raw_timings_ = &timings; - this->packed_data_ = nullptr; // Clear packed if vector is set + this->packed_data_ = nullptr; + this->base64url_ptr_ = nullptr; + return *this; +} + +InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) { + this->base64url_ptr_ = &base64url; + this->raw_timings_ = nullptr; + this->packed_data_ = nullptr; return *this; } @@ -26,7 +34,8 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t this->packed_data_ = data; this->packed_length_ = length; this->packed_count_ = count; - this->raw_timings_ = nullptr; // Clear vector if packed is set + this->raw_timings_ = nullptr; + this->base64url_ptr_ = nullptr; return *this; } @@ -92,6 +101,23 @@ void Infrared::control(const InfraredCall &call) { call.get_packed_count()); ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(), call.get_repeat_count()); + } else if (call.is_base64url()) { + // Decode base64url (URL-safe) into transmit buffer + if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) { + ESP_LOGE(TAG, "Invalid base64url data"); + return; + } + // Sanity check: validate timing values are within reasonable bounds + constexpr int32_t max_timing_us = 500000; // 500ms absolute max + for (int32_t timing : transmit_data->get_data()) { + int32_t abs_timing = timing < 0 ? -timing : timing; + if (abs_timing > max_timing_us) { + ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us); + return; + } + } + ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(), + call.get_repeat_count()); } else { // From vector (lambdas/automations) transmit_data->set_data(call.get_raw_timings()); diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index 3a891301f4..59535f499a 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -28,12 +28,29 @@ class InfraredCall { /// Set the carrier frequency in Hz InfraredCall &set_carrier_frequency(uint32_t frequency); - /// Set the raw timings (positive = mark, negative = space) - /// Note: The timings vector must outlive the InfraredCall (zero-copy reference) + + // ===== Raw Timings Methods ===== + // All set_raw_timings_* methods store pointers/references to external data. + // The referenced data must remain valid until perform() completes. + // Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous + // Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone! + + /// Set the raw timings from a vector (positive = mark, negative = space) + /// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform(). + /// @note Usage: Primarily for lambdas/automations where the vector is in scope. InfraredCall &set_raw_timings(const std::vector &timings); - /// Set the raw timings from packed protobuf sint32 data (zero-copy from wire) - /// Note: The data must outlive the InfraredCall + + /// Set the raw timings from base64url-encoded little-endian int32 data + /// @note Lifetime: Stores a pointer to the string. The string must outlive perform(). + /// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_'). + /// @note Decoding happens at perform() time, directly into the transmit buffer. + InfraredCall &set_raw_timings_base64url(const std::string &base64url); + + /// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded) + /// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform(). + /// @note Usage: For API component where data comes directly from the protobuf message. InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); + /// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.) InfraredCall &set_repeat_count(uint32_t count); @@ -42,12 +59,18 @@ class InfraredCall { /// Get the carrier frequency const optional &get_carrier_frequency() const { return this->carrier_frequency_; } - /// Get the raw timings (only valid if set via set_raw_timings, not packed) + /// Get the raw timings (only valid if set via set_raw_timings) const std::vector &get_raw_timings() const { return *this->raw_timings_; } - /// Check if raw timings have been set (either vector or packed) - bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; } + /// Check if raw timings have been set (any format) + bool has_raw_timings() const { + return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr; + } /// Check if using packed data format bool is_packed() const { return this->packed_data_ != nullptr; } + /// Check if using base64url data format + bool is_base64url() const { return this->base64url_ptr_ != nullptr; } + /// Get the base64url data string + const std::string &get_base64url_data() const { return *this->base64url_ptr_; } /// Get packed data (only valid if set via set_raw_timings_packed) const uint8_t *get_packed_data() const { return this->packed_data_; } uint16_t get_packed_length() const { return this->packed_length_; } @@ -59,9 +82,11 @@ class InfraredCall { uint32_t repeat_count_{1}; Infrared *parent_; optional carrier_frequency_; - // Vector-based timings (for lambdas/automations) + // Pointer to vector-based timings (caller-owned, must outlive perform()) const std::vector *raw_timings_{nullptr}; - // Packed protobuf timings (for API zero-copy) + // Pointer to base64url-encoded string (caller-owned, must outlive perform()) + const std::string *base64url_ptr_{nullptr}; + // Pointer to packed protobuf buffer (caller-owned, must outlive perform()) const uint8_t *packed_data_{nullptr}; uint16_t packed_length_{0}; uint16_t packed_count_{0}; diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 018f5113e3..aca6ec10f3 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -52,7 +52,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); + ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); this->state_callback_.call(); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 947e44b131..3c1838219c 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -413,6 +413,7 @@ class TextValidator(LValidator): str_args = [str(x) for x in value[CONF_ARGS]] arg_expr = cg.RawExpression(",".join(str_args)) format_str = cpp_string_escape(format_str) + # str_sprintf justified: user-defined format, can't optimize without permanent RAM cost sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()" if nanval := value.get(CONF_IF_NAN): nanval = cpp_string_escape(nanval) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 50dba94a2b..bb373abb88 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -65,7 +65,10 @@ std::string lv_event_code_name_for(uint8_t event_code) { if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) { return EVENT_NAMES[event_code]; } - return str_sprintf("%2d", event_code); + // max 4 bytes: "%u" with uint8_t (max 255, 3 digits) + null + char buf[4]; + snprintf(buf, sizeof(buf), "%u", event_code); + return buf; } static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index cd566633f9..db13c7f6cc 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -101,4 +101,225 @@ DriverChip( (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), ] ) + +# jc8012P4A1 Driver Configuration (jd9365) +# Using parameters from esp_lcd_jd9365.h and the working full init sequence +# ---------------------------------------------------------------------------------------------------------------------- +# * Resolution: 800x1280 +# * PCLK Frequency: 60 MHz +# * DSI Lane Bit Rate: 1 Gbps (using 2-Lane DSI configuration) +# * Horizontal Timing (hsync_pulse_width=20, hsync_back_porch=20, hsync_front_porch=40) +# * Vertical Timing (vsync_pulse_width=4, vsync_back_porch=8, vsync_front_porch=20) +# ---------------------------------------------------------------------------------------------------------------------- +DriverChip( + "JC8012P4A1", + width=800, + height=1280, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=8, + vsync_pulse_width=4, + vsync_front_porch=20, + pclk_frequency="60MHz", + lane_bit_rate="1Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=27, + initsequence=[ + (0xE0, 0x00), + (0xE1, 0x93), + (0xE2, 0x65), + (0xE3, 0xF8), + (0x80, 0x01), + (0xE0, 0x01), + (0x00, 0x00), + (0x01, 0x39), + (0x03, 0x10), + (0x04, 0x41), + (0x0C, 0x74), + (0x17, 0x00), + (0x18, 0xD7), + (0x19, 0x00), + (0x1A, 0x00), + (0x1B, 0xD7), + (0x1C, 0x00), + (0x24, 0xFE), + (0x35, 0x26), + (0x37, 0x69), + (0x38, 0x05), + (0x39, 0x06), + (0x3A, 0x08), + (0x3C, 0x78), + (0x3D, 0xFF), + (0x3E, 0xFF), + (0x3F, 0xFF), + (0x40, 0x06), + (0x41, 0xA0), + (0x43, 0x14), + (0x44, 0x0B), + (0x45, 0x30), + (0x4B, 0x04), + (0x55, 0x02), + (0x57, 0x89), + (0x59, 0x0A), + (0x5A, 0x28), + (0x5B, 0x15), + (0x5D, 0x50), + (0x5E, 0x37), + (0x5F, 0x29), + (0x60, 0x1E), + (0x61, 0x1D), + (0x62, 0x12), + (0x63, 0x1A), + (0x64, 0x08), + (0x65, 0x25), + (0x66, 0x26), + (0x67, 0x28), + (0x68, 0x49), + (0x69, 0x3A), + (0x6A, 0x43), + (0x6B, 0x3A), + (0x6C, 0x3B), + (0x6D, 0x32), + (0x6E, 0x1F), + (0x6F, 0x0E), + (0x70, 0x50), + (0x71, 0x37), + (0x72, 0x29), + (0x73, 0x1E), + (0x74, 0x1D), + (0x75, 0x12), + (0x76, 0x1A), + (0x77, 0x08), + (0x78, 0x25), + (0x79, 0x26), + (0x7A, 0x28), + (0x7B, 0x49), + (0x7C, 0x3A), + (0x7D, 0x43), + (0x7E, 0x3A), + (0x7F, 0x3B), + (0x80, 0x32), + (0x81, 0x1F), + (0x82, 0x0E), + (0xE0, 0x02), + (0x00, 0x1F), + (0x01, 0x1F), + (0x02, 0x52), + (0x03, 0x51), + (0x04, 0x50), + (0x05, 0x4B), + (0x06, 0x4A), + (0x07, 0x49), + (0x08, 0x48), + (0x09, 0x47), + (0x0A, 0x46), + (0x0B, 0x45), + (0x0C, 0x44), + (0x0D, 0x40), + (0x0E, 0x41), + (0x0F, 0x1F), + (0x10, 0x1F), + (0x11, 0x1F), + (0x12, 0x1F), + (0x13, 0x1F), + (0x14, 0x1F), + (0x15, 0x1F), + (0x16, 0x1F), + (0x17, 0x1F), + (0x18, 0x52), + (0x19, 0x51), + (0x1A, 0x50), + (0x1B, 0x4B), + (0x1C, 0x4A), + (0x1D, 0x49), + (0x1E, 0x48), + (0x1F, 0x47), + (0x20, 0x46), + (0x21, 0x45), + (0x22, 0x44), + (0x23, 0x40), + (0x24, 0x41), + (0x25, 0x1F), + (0x26, 0x1F), + (0x27, 0x1F), + (0x28, 0x1F), + (0x29, 0x1F), + (0x2A, 0x1F), + (0x2B, 0x1F), + (0x2C, 0x1F), + (0x2D, 0x1F), + (0x2E, 0x52), + (0x2F, 0x40), + (0x30, 0x41), + (0x31, 0x48), + (0x32, 0x49), + (0x33, 0x4A), + (0x34, 0x4B), + (0x35, 0x44), + (0x36, 0x45), + (0x37, 0x46), + (0x38, 0x47), + (0x39, 0x51), + (0x3A, 0x50), + (0x3B, 0x1F), + (0x3C, 0x1F), + (0x3D, 0x1F), + (0x3E, 0x1F), + (0x3F, 0x1F), + (0x40, 0x1F), + (0x41, 0x1F), + (0x42, 0x1F), + (0x43, 0x1F), + (0x44, 0x52), + (0x45, 0x40), + (0x46, 0x41), + (0x47, 0x48), + (0x48, 0x49), + (0x49, 0x4A), + (0x4A, 0x4B), + (0x4B, 0x44), + (0x4C, 0x45), + (0x4D, 0x46), + (0x4E, 0x47), + (0x4F, 0x51), + (0x50, 0x50), + (0x51, 0x1F), + (0x52, 0x1F), + (0x53, 0x1F), + (0x54, 0x1F), + (0x55, 0x1F), + (0x56, 0x1F), + (0x57, 0x1F), + (0x58, 0x40), + (0x59, 0x00), + (0x5A, 0x00), + (0x5B, 0x10), + (0x5C, 0x05), + (0x5D, 0x50), + (0x5E, 0x01), + (0x5F, 0x02), + (0x60, 0x50), + (0x61, 0x06), + (0x62, 0x04), + (0x63, 0x03), + (0x64, 0x64), + (0x65, 0x65), + (0x66, 0x0B), + (0x67, 0x73), + (0x68, 0x07), + (0x69, 0x06), + (0x6A, 0x64), + (0x6B, 0x08), + (0x6C, 0x00), + (0x6D, 0x32), + (0x6E, 0x08), + (0xE0, 0x04), + (0x2C, 0x6B), + (0x35, 0x08), + (0x37, 0x00), + (0xE0, 0x00), + ] +) # fmt: on diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 20c111de43..8e4b3437ab 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -189,8 +189,7 @@ bool MQTTComponent::send_discovery_() { StringRef object_id = this->get_default_object_id_to_(object_id_buf); if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; - sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); - friendly_name_hash[8] = 0; // ensure the hash-string ends with null + snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_())); // Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678") // MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43 char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11]; diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index b95df55a61..0b4ba3a171 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -11,7 +11,12 @@ from esphome.const import ( ) from esphome.core import CORE, TimePeriod -from . import Nextion, nextion_ns, nextion_ref +from . import ( # noqa: F401 pylint: disable=unused-import + FILTER_SOURCE_FILES, + Nextion, + nextion_ns, + nextion_ref, +) from .base_component import ( CONF_AUTO_WAKE_ON_TOUCH, CONF_COMMAND_SPACING, diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index b08f84029b..cc500ba429 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -23,7 +23,7 @@ void NTC::process_(float value) { double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; auto temp = float(1.0 / v - 273.15); - ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp); + ESP_LOGV(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp); this->publish_state(temp); } diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 992100ead0..b0af604189 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -31,7 +31,7 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o void Number::publish_state(float state) { this->set_has_state(true); this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); + ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state); this->state_callback_.call(state); #if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_number_update(this); diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index c6443f1282..2bf438a52f 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -561,8 +561,9 @@ const char *OpenTherm::message_id_to_str(MessageId id) { } void OpenTherm::debug_data(OpenthermData &data) { - ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(), - format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str()); + char type_buf[9], id_buf[9], hb_buf[9], lb_buf[9]; + ESP_LOGD(TAG, "%s %s %s %s", format_bin_to(type_buf, data.type), format_bin_to(id_buf, data.id), + format_bin_to(hb_buf, data.valueHB), format_bin_to(lb_buf, data.valueLB)); ESP_LOGD(TAG, "type: %s; id: %u; HB: %u; LB: %u; uint_16: %u; float: %f", this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.u16(), data.f88()); diff --git a/esphome/components/qr_code/qr_code.cpp b/esphome/components/qr_code/qr_code.cpp index c2db741e17..0322c8a141 100644 --- a/esphome/components/qr_code/qr_code.cpp +++ b/esphome/components/qr_code/qr_code.cpp @@ -27,7 +27,16 @@ void QrCode::set_ecc(qrcodegen_Ecc ecc) { void QrCode::generate_qr_code() { ESP_LOGV(TAG, "Generating QR code"); + +#ifdef USE_ESP32 + // ESP32 has 8KB stack, safe to allocate ~4KB buffer on stack uint8_t tempbuffer[qrcodegen_BUFFER_LEN_MAX]; +#else + // Other platforms (ESP8266: 4KB, RP2040: 2KB, LibreTiny: ~4KB) have smaller stacks + // Allocate buffer on heap to avoid stack overflow + auto tempbuffer_owner = std::make_unique(qrcodegen_BUFFER_LEN_MAX); + uint8_t *tempbuffer = tempbuffer_owner.get(); +#endif if (!qrcodegen_encodeText(this->value_.c_str(), tempbuffer, this->qr_, this->ecc_, qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true)) { diff --git a/esphome/components/remote_base/aeha_protocol.cpp b/esphome/components/remote_base/aeha_protocol.cpp index 04fe731817..3b926e7981 100644 --- a/esphome/components/remote_base/aeha_protocol.cpp +++ b/esphome/components/remote_base/aeha_protocol.cpp @@ -85,8 +85,8 @@ optional AEHAProtocol::decode(RemoteReceiveData src) { std::string AEHAProtocol::format_data_(const std::vector &data) { std::string out; for (uint8_t byte : data) { - char buf[6]; - sprintf(buf, "0x%02X,", byte); + char buf[8]; // "0x%02X," = 5 chars + null + margin + snprintf(buf, sizeof(buf), "0x%02X,", byte); out += buf; } out.pop_back(); diff --git a/esphome/components/remote_base/raw_protocol.cpp b/esphome/components/remote_base/raw_protocol.cpp index ef0cb8454e..7e6be3b77e 100644 --- a/esphome/components/remote_base/raw_protocol.cpp +++ b/esphome/components/remote_base/raw_protocol.cpp @@ -1,4 +1,5 @@ #include "raw_protocol.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -8,36 +9,30 @@ static const char *const TAG = "remote.raw"; bool RawDumper::dump(RemoteReceiveData src) { char buffer[256]; - uint32_t buffer_offset = 0; - buffer_offset += sprintf(buffer, "Received Raw: "); + size_t pos = buf_append_printf(buffer, sizeof(buffer), 0, "Received Raw: "); for (int32_t i = 0; i < src.size() - 1; i++) { const int32_t value = src[i]; - const uint32_t remaining_length = sizeof(buffer) - buffer_offset; - int written; + size_t prev_pos = pos; if (i + 1 < src.size() - 1) { - written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value); + pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32 ", ", value); } else { - written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value); + pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32, value); } - if (written < 0 || written >= int(remaining_length)) { - // write failed, flush... - buffer[buffer_offset] = '\0'; + if (pos >= sizeof(buffer) - 1) { + // buffer full, flush and continue + buffer[prev_pos] = '\0'; ESP_LOGI(TAG, "%s", buffer); - buffer_offset = 0; - written = sprintf(buffer, " "); if (i + 1 < src.size() - 1) { - written += sprintf(buffer + written, "%" PRId32 ", ", value); + pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32 ", ", value); } else { - written += sprintf(buffer + written, "%" PRId32, value); + pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32, value); } } - - buffer_offset += written; } - if (buffer_offset != 0) { + if (pos != 0) { ESP_LOGI(TAG, "%s", buffer); } return true; diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 2f1c107bf4..b4a549f0be 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -1,8 +1,7 @@ #include "remote_base.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include - namespace esphome { namespace remote_base { @@ -159,42 +158,41 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t } } +bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) { + return base64_decode_int32_vector(base64url, this->data_); +} + /* RemoteTransmitterBase */ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE const auto &vec = this->temp_.get_data(); char buffer[256]; - uint32_t buffer_offset = 0; - buffer_offset += sprintf(buffer, "Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait); + size_t pos = buf_append_printf(buffer, sizeof(buffer), 0, + "Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait); for (size_t i = 0; i < vec.size(); i++) { const int32_t value = vec[i]; - const uint32_t remaining_length = sizeof(buffer) - buffer_offset; - int written; + size_t prev_pos = pos; if (i + 1 < vec.size()) { - written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value); + pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32 ", ", value); } else { - written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value); + pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32, value); } - if (written < 0 || written >= int(remaining_length)) { - // write failed, flush... - buffer[buffer_offset] = '\0'; + if (pos >= sizeof(buffer) - 1) { + // buffer full, flush and continue + buffer[prev_pos] = '\0'; ESP_LOGVV(TAG, "%s", buffer); - buffer_offset = 0; - written = sprintf(buffer, " "); if (i + 1 < vec.size()) { - written += sprintf(buffer + written, "%" PRId32 ", ", value); + pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32 ", ", value); } else { - written += sprintf(buffer + written, "%" PRId32, value); + pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32, value); } } - - buffer_offset += written; } - if (buffer_offset != 0) { + if (pos != 0) { ESP_LOGVV(TAG, "%s", buffer); } #endif diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index a11e0271af..0cac28506f 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -36,6 +36,11 @@ class RemoteTransmitData { /// @param len Length of the buffer in bytes /// @param count Number of values (for reserve optimization) void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count); + /// Set data from base64url-encoded little-endian int32 values + /// Base64url is URL-safe: uses '-' instead of '+', '_' instead of '/' + /// @param base64url Base64url-encoded string of little-endian int32 values + /// @return true if successful, false if decode failed or invalid size + bool set_data_from_base64url(const std::string &base64url); void reset() { this->data_.clear(); this->carrier_frequency_ = 0; diff --git a/esphome/components/remote_receiver/binary_sensor.py b/esphome/components/remote_receiver/binary_sensor.py index 218b40d6cc..fe3e2af950 100644 --- a/esphome/components/remote_receiver/binary_sensor.py +++ b/esphome/components/remote_receiver/binary_sensor.py @@ -1,5 +1,7 @@ from esphome.components import binary_sensor, remote_base +from . import FILTER_SOURCE_FILES # noqa: F401 pylint: disable=unused-import + DEPENDENCIES = ["remote_receiver"] CONFIG_SCHEMA = remote_base.validate_binary_sensor diff --git a/esphome/components/resistance/resistance_sensor.cpp b/esphome/components/resistance/resistance_sensor.cpp index 6e57214449..706a059de3 100644 --- a/esphome/components/resistance/resistance_sensor.cpp +++ b/esphome/components/resistance/resistance_sensor.cpp @@ -39,7 +39,7 @@ void ResistanceSensor::process_(float value) { } res *= this->resistor_; - ESP_LOGD(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res); + ESP_LOGV(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res); this->publish_state(res); } diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index ef6ebea247..f32511531a 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -9,7 +9,7 @@ #include #include -#ifdef USE_OTA_ROLLBACK +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) #include #endif @@ -26,6 +26,17 @@ void SafeModeComponent::dump_config() { this->safe_mode_boot_is_good_after_ / 1000, // because milliseconds this->safe_mode_num_attempts_, this->safe_mode_enable_time_ / 1000); // because milliseconds +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) + const char *state_str; + if (this->ota_state_ == ESP_OTA_IMG_NEW) { + state_str = "not supported"; + } else if (this->ota_state_ == ESP_OTA_IMG_PENDING_VERIFY) { + state_str = "supported"; + } else { + state_str = "support unknown"; + } + ESP_LOGCONFIG(TAG, " Bootloader rollback: %s", state_str); +#endif if (this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) { auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_; @@ -36,7 +47,7 @@ void SafeModeComponent::dump_config() { } } -#ifdef USE_OTA_ROLLBACK +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition(); if (last_invalid != nullptr) { ESP_LOGW(TAG, @@ -55,7 +66,7 @@ void SafeModeComponent::loop() { ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); this->clean_rtc(); this->boot_successful_ = true; -#ifdef USE_OTA_ROLLBACK +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) // Mark OTA partition as valid to prevent rollback esp_ota_mark_app_valid_cancel_rollback(); #endif @@ -90,6 +101,12 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en this->safe_mode_num_attempts_ = num_attempts; this->rtc_ = global_preferences->make_preference(233825507UL, false); +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) + // Check partition state to detect if bootloader supports rollback + const esp_partition_t *running = esp_ota_get_running_partition(); + esp_ota_get_state_partition(running, &this->ota_state_); +#endif + uint32_t rtc_val = this->read_rtc_(); this->safe_mode_rtc_value_ = rtc_val; diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 4aefd11458..d6f669f39f 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -5,6 +5,10 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) +#include +#endif + namespace esphome::safe_mode { /// SafeModeComponent provides a safe way to recover from repeated boot failures @@ -42,6 +46,9 @@ class SafeModeComponent : public Component { // Group 1-byte members together to minimize padding bool boot_successful_{false}; ///< set to true after boot is considered successful uint8_t safe_mode_num_attempts_{0}; +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) + esp_ota_img_states_t ota_state_{ESP_OTA_IMG_UNDEFINED}; +#endif // Larger objects at the end ESPPreferenceObject rtc_; #ifdef USE_SAFE_MODE_CALLBACK diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 7c50fe02c0..c51131a292 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -8,17 +8,20 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_INDEX, + CONF_LAMBDA, CONF_MODE, CONF_MQTT_ID, CONF_ON_VALUE, CONF_OPERATION, CONF_OPTION, + CONF_OPTIONS, CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObjClass, TemplateArguments +from esphome.cpp_types import global_ns CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -38,6 +41,9 @@ SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action) SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action) +# Conditions +SelectIsCondition = select_ns.class_("SelectIsCondition", automation.Condition) + # Enums SelectOperation = select_ns.enum("SelectOperation") SELECT_OPERATION_OPTIONS = { @@ -165,6 +171,41 @@ async def select_set_index_to_code(config, action_id, template_arg, args): return var +@automation.register_condition( + "select.is", + SelectIsCondition, + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_OPTIONS): cv.All( + cv.ensure_list(cv.string_strict), cv.Length(min=1) + ), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).add_extra(cv.has_exactly_one_key(CONF_OPTIONS, CONF_LAMBDA)), +) +async def select_is_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + if options := config.get(CONF_OPTIONS): + # List of constant options + # Create a constexpr and pass that with a template length + arr_id = ID( + f"{condition_id}_data", + is_declaration=True, + type=global_ns.namespace("constexpr char * const"), + ) + arg = cg.static_const_array(arr_id, cg.ArrayInitializer(*options)) + template_arg = TemplateArguments(len(options), *template_arg) + else: + # Lambda + arg = await cg.process_lambda( + config[CONF_LAMBDA], + [(global_ns.namespace("StringRef &").operator("const"), "current")] + args, + return_type=cg.bool_, + ) + template_arg = TemplateArguments(0, *template_arg) + return cg.new_Pvariable(condition_id, template_arg, paren, arg) + + @automation.register_action( "select.operation", SelectOperationAction, diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index dda5403557..81e8a3561d 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -66,4 +66,34 @@ template class SelectOperationAction : public Action { Select *select_; }; +template class SelectIsCondition : public Condition { + public: + SelectIsCondition(Select *parent, const char *const *option_list) : parent_(parent), option_list_(option_list) {} + + bool check(const Ts &...x) override { + auto current = this->parent_->current_option(); + for (size_t i = 0; i != N; i++) { + if (current == this->option_list_[i]) { + return true; + } + } + return false; + } + + protected: + Select *parent_; + const char *const *option_list_; +}; + +template class SelectIsCondition<0, Ts...> : public Condition { + public: + SelectIsCondition(Select *parent, std::function &&f) + : parent_(parent), f_(f) {} + + bool check(const Ts &...x) override { return this->f_(this->parent_->current_option(), x...); } + + protected: + Select *parent_; + std::function f_; +}; } // namespace esphome::select diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 3d70e94d47..91e27b30de 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -31,7 +31,7 @@ void Select::publish_state(size_t index) { #pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->state = option; // Update deprecated member for backward compatibility #pragma GCC diagnostic pop - ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); + ESP_LOGD(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index); this->state_callback_.call(index); #if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_select_update(this); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 64678f8d0c..9fdb7bbafd 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -126,8 +126,8 @@ float Sensor::get_raw_state() const { return this->raw_state; } void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, - this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); + ESP_LOGD(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state, + this->get_unit_of_measurement_ref().c_str()); this->callback_.call(state); #if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_sensor_update(this); diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp index 48559d7c73..23995e6534 100644 --- a/esphome/components/sha256/sha256.cpp +++ b/esphome/components/sha256/sha256.cpp @@ -10,26 +10,24 @@ namespace esphome::sha256 { #if defined(USE_ESP32) || defined(USE_LIBRETINY) -// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x): +// CRITICAL ESP32 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x): // -// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains -// internal state that the DMA engine references. This imposes three critical constraints: +// ESP32 variants (except original ESP32) use DMA-based hardware SHA acceleration that requires +// 32-byte aligned digest buffers. This is handled automatically via HashBase::digest_ which has +// alignas(32) on these platforms. Two additional constraints apply: // -// 1. ALIGNMENT: The SHA256 object MUST be declared with `alignas(32)` for proper DMA alignment. -// Without this, the DMA engine may crash with an abort in sha_hal_read_digest(). -// -// 2. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to +// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to // write to incorrect memory locations. This results in null pointer dereferences and crashes. // ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]). // -// 3. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same +// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same // function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack // frame changes (function call/return), the DMA references become invalid and will produce // truncated hash output (20 bytes instead of 32) or corrupt memory. // // CORRECT USAGE: // void my_function() { -// alignas(32) sha256::SHA256 hasher; // Created locally with proper alignment +// sha256::SHA256 hasher; // hasher.init(); // hasher.add(data, len); // Any size, no chunking needed // hasher.calculate(); @@ -37,9 +35,9 @@ namespace esphome::sha256 { // // hasher destroyed when function returns // } // -// INCORRECT USAGE (WILL FAIL ON ESP32-S3): +// INCORRECT USAGE (WILL FAIL): // void my_function() { -// sha256::SHA256 hasher; // WRONG: Missing alignas(32) +// sha256::SHA256 hasher; // helper(&hasher); // WRONG: Passed to different stack frame // } // void helper(HashBase *h) { diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index 17d80636f1..bafb359485 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -24,13 +24,14 @@ namespace esphome::sha256 { /// SHA256 hash implementation. /// -/// CRITICAL for ESP32-S3 with IDF 5.5.x hardware SHA acceleration: -/// 1. SHA256 objects MUST be declared with `alignas(32)` for proper DMA alignment -/// 2. The object MUST stay in the same stack frame (no passing to other functions) -/// 3. NO Variable Length Arrays (VLAs) in the same function +/// CRITICAL for ESP32 variants (except original) with IDF 5.5.x hardware SHA acceleration: +/// 1. The object MUST stay in the same stack frame (no passing to other functions) +/// 2. NO Variable Length Arrays (VLAs) in the same function +/// +/// Note: Alignment is handled automatically via the HashBase::digest_ member. /// /// Example usage: -/// alignas(32) sha256::SHA256 hasher; +/// sha256::SHA256 hasher; /// hasher.init(); /// hasher.add(data, len); /// hasher.calculate(); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index ca9f85abd8..2813b4450b 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -332,6 +332,7 @@ Sprinkler::Sprinkler(const std::string &name) { // The `name` is needed to set timers up, hence non-default constructor // replaces `set_name()` method previously existed this->name_ = name; + this->timer_.init(2); this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); } @@ -1574,7 +1575,8 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) { void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { if (this->timer_duration_(timer_index) > 0) { - this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), + // FixedVector ensures timer_ can't be resized, so .c_str() pointers remain valid + this->set_timeout(this->timer_[timer_index].name.c_str(), this->timer_duration_(timer_index), this->timer_cbf_(timer_index)); this->timer_[timer_index].start_time = millis(); this->timer_[timer_index].active = true; @@ -1585,7 +1587,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) { this->timer_[timer_index].active = false; - return this->cancel_timeout(this->timer_[timer_index].name); + return this->cancel_timeout(this->timer_[timer_index].name.c_str()); } bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; } diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 25e2d42446..273c0e9208 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -3,6 +3,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/number/number.h" #include "esphome/components/switch/switch.h" @@ -553,8 +554,8 @@ class Sprinkler : public Component { /// Sprinkler valve operator objects std::vector valve_op_{2}; - /// Valve control timers - std::vector timer_{}; + /// Valve control timers - FixedVector enforces that this can never grow beyond init() size + FixedVector timer_; /// Other Sprinkler instances we should be aware of (used to check if pumps are in use) std::vector other_controllers_; diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 3c3a437ff3..069533fa78 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -62,7 +62,7 @@ void Switch::publish_state(bool state) { if (restore_mode & RESTORE_MODE_PERSISTENT_MASK) this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); + ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); #if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_switch_update(this); diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index c2ade56f69..e3f74b685b 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -20,9 +20,9 @@ void Text::publish_state(const char *state, size_t len) { this->state.assign(state, len); } if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { - ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); + ESP_LOGD(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); } else { - ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), this->state.c_str()); + ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str()); } this->state_callback_.call(this->state); #if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 66301564a4..86e2387dc7 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -116,7 +116,7 @@ void TextSensor::internal_send_state_to_frontend(const char *state, size_t len) void TextSensor::notify_frontend_() { this->set_has_state(true); - ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), this->state.c_str()); + ESP_LOGD(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str()); this->callback_.call(this->state); #if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_sensor_update(this); diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 639af4457f..f217d14c55 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -31,6 +31,18 @@ void RealTimeClock::dump_config() { void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); + // Skip if time is already synchronized to avoid unnecessary writes, log spam, + // and prevent clock jumping backwards due to network latency + constexpr time_t min_valid_epoch = 1546300800; // January 1, 2019 + time_t current_time = this->timestamp_now(); + // Check if time is valid (year >= 2019) before comparing + if (current_time >= min_valid_epoch) { + // Unsigned subtraction handles wraparound correctly, then cast to signed + int32_t diff = static_cast(epoch - static_cast(current_time)); + if (diff >= -1 && diff <= 1) { + return; + } + } // Update UTC epoch time. #ifdef USE_ZEPHYR struct timespec ts; diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 6d13341a8a..515e4c2c18 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "update"; void UpdateEntity::publish_state() { ESP_LOGD(TAG, - "'%s' - Publishing:\n" + "'%s' >>\n" " Current Version: %s", this->name_.c_str(), this->update_info_.current_version.c_str()); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index fed113afc2..a9086747ce 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -133,7 +133,7 @@ void Valve::add_on_state_callback(std::function &&f) { this->state_callb void Valve::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str()); + ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index d092203d06..7b947057e1 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -153,7 +153,7 @@ void WaterHeater::setup() { void WaterHeater::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, - "'%s' - Sending state:\n" + "'%s' >>\n" " Mode: %s", this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); if (!std::isnan(this->current_temperature_)) { diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 16ac9d054c..3f1e094afc 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -203,7 +203,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_OTA): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, - cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"), + cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"), cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), } ).extend(cv.COMPONENT_SCHEMA), diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0525c93096..0e71d82233 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -658,6 +658,24 @@ std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std #endif #ifdef USE_SWITCH +enum SwitchAction : uint8_t { SWITCH_ACTION_NONE, SWITCH_ACTION_TOGGLE, SWITCH_ACTION_TURN_ON, SWITCH_ACTION_TURN_OFF }; + +static void execute_switch_action(switch_::Switch *obj, SwitchAction action) { + switch (action) { + case SWITCH_ACTION_TOGGLE: + obj->toggle(); + break; + case SWITCH_ACTION_TURN_ON: + obj->turn_on(); + break; + case SWITCH_ACTION_TURN_OFF: + obj->turn_off(); + break; + default: + break; + } +} + void WebServer::on_switch_update(switch_::Switch *obj) { if (!this->include_internal_ && obj->is_internal()) return; @@ -676,34 +694,22 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM return; } - // Handle action methods with single defer and response - enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF }; - SwitchAction action = NONE; + SwitchAction action = SWITCH_ACTION_NONE; if (match.method_equals(ESPHOME_F("toggle"))) { - action = TOGGLE; + action = SWITCH_ACTION_TOGGLE; } else if (match.method_equals(ESPHOME_F("turn_on"))) { - action = TURN_ON; + action = SWITCH_ACTION_TURN_ON; } else if (match.method_equals(ESPHOME_F("turn_off"))) { - action = TURN_OFF; + action = SWITCH_ACTION_TURN_OFF; } - if (action != NONE) { - this->defer([obj, action]() { - switch (action) { - case TOGGLE: - obj->toggle(); - break; - case TURN_ON: - obj->turn_on(); - break; - case TURN_OFF: - obj->turn_off(); - break; - default: - break; - } - }); + if (action != SWITCH_ACTION_NONE) { +#ifdef USE_ESP8266 + execute_switch_action(obj, action); +#else + this->defer([obj, action]() { execute_switch_action(obj, action); }); +#endif request->send(200); } else { request->send(404); @@ -743,7 +749,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("press"))) { - this->defer([obj]() { obj->press(); }); + DEFER_ACTION(obj, obj->press()); request->send(200); return; } else { @@ -753,9 +759,6 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) { - return web_server->button_json_((button::Button *) (source), DETAIL_STATE); -} std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { return web_server->button_json_((button::Button *) (source), DETAIL_ALL); } @@ -831,7 +834,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { - this->defer([obj]() { obj->toggle().perform(); }); + DEFER_ACTION(obj, obj->toggle().perform()); request->send(200); } else { bool is_on = match.method_equals(ESPHOME_F("turn_on")); @@ -862,7 +865,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc return; } } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); } return; @@ -912,7 +915,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals(ESPHOME_F("toggle"))) { - this->defer([obj]() { obj->toggle().perform(); }); + DEFER_ACTION(obj, obj->toggle().perform()); request->send(200); } else { bool is_on = match.method_equals(ESPHOME_F("turn_on")); @@ -941,7 +944,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect); } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); } return; @@ -1030,7 +1033,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1089,7 +1092,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1162,7 +1165,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1226,7 +1229,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1289,7 +1292,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1349,7 +1352,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1407,7 +1410,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM auto call = obj->make_call(); parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1476,7 +1479,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low); parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1592,6 +1595,24 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con #endif #ifdef USE_LOCK +enum LockAction : uint8_t { LOCK_ACTION_NONE, LOCK_ACTION_LOCK, LOCK_ACTION_UNLOCK, LOCK_ACTION_OPEN }; + +static void execute_lock_action(lock::Lock *obj, LockAction action) { + switch (action) { + case LOCK_ACTION_LOCK: + obj->lock(); + break; + case LOCK_ACTION_UNLOCK: + obj->unlock(); + break; + case LOCK_ACTION_OPEN: + obj->open(); + break; + default: + break; + } +} + void WebServer::on_lock_update(lock::Lock *obj) { if (!this->include_internal_ && obj->is_internal()) return; @@ -1610,34 +1631,22 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat return; } - // Handle action methods with single defer and response - enum LockAction { NONE, LOCK, UNLOCK, OPEN }; - LockAction action = NONE; + LockAction action = LOCK_ACTION_NONE; if (match.method_equals(ESPHOME_F("lock"))) { - action = LOCK; + action = LOCK_ACTION_LOCK; } else if (match.method_equals(ESPHOME_F("unlock"))) { - action = UNLOCK; + action = LOCK_ACTION_UNLOCK; } else if (match.method_equals(ESPHOME_F("open"))) { - action = OPEN; + action = LOCK_ACTION_OPEN; } - if (action != NONE) { - this->defer([obj, action]() { - switch (action) { - case LOCK: - obj->lock(); - break; - case UNLOCK: - obj->unlock(); - break; - case OPEN: - obj->open(); - break; - default: - break; - } - }); + if (action != LOCK_ACTION_NONE) { +#ifdef USE_ESP8266 + execute_lock_action(obj, action); +#else + this->defer([obj, action]() { execute_lock_action(obj, action); }); +#endif request->send(200); } else { request->send(404); @@ -1720,7 +1729,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1799,7 +1808,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques return; } - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -1875,7 +1884,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons // Parse on/off parameter parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on); - this->defer([call]() mutable { call.perform(); }); + DEFER_ACTION(call, call.perform()); request->send(200); return; } @@ -2035,7 +2044,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM return; } - this->defer([obj]() mutable { obj->perform(); }); + DEFER_ACTION(obj, obj->perform()); request->send(200); return; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 91625476f4..c434d664cf 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -42,6 +42,14 @@ using ParamNameType = const __FlashStringHelper *; using ParamNameType = const char *; #endif +// ESP8266 is single-threaded, so actions can execute directly in request context. +// Multi-core platforms need to defer to main loop thread for thread safety. +#ifdef USE_ESP8266 +#define DEFER_ACTION(capture, action) action +#else +#define DEFER_ACTION(capture, action) this->defer([capture]() mutable { action; }) +#endif + /// Result of matching a URL against an entity struct EntityMatchResult { bool matched; ///< True if entity matched the URL @@ -295,7 +303,7 @@ class WebServer : public Controller, /// Handle a button request under '/button//press'. void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match); - static std::string button_state_json_generator(WebServer *web_server, void *source); + // Buttons are stateless, so there is no button_state_json_generator static std::string button_all_json_generator(WebServer *web_server, void *source); #endif diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index 0c1c2dce33..606cd3080c 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -44,7 +44,15 @@ class HashBase { virtual size_t get_size() const = 0; protected: - uint8_t digest_[32]; // Storage sized for max(MD5=16, SHA256=32) bytes +// ESP32 variants with DMA-based hardware SHA (all except original ESP32) require 32-byte aligned buffers. +// Original ESP32 uses a different hardware SHA implementation without DMA alignment requirements. +// Other platforms (ESP8266, RP2040, LibreTiny) use software SHA and don't need alignment. +// Storage sized for max(MD5=16, SHA256=32) bytes +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32) + alignas(32) uint8_t digest_[32]; +#else + uint8_t digest_[32]; +#endif }; } // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 309407fbec..4e3761675d 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -404,15 +404,31 @@ std::string format_hex_pretty(const std::string &data, char separator, bool show return format_hex_pretty_uint8(reinterpret_cast(data.data()), data.length(), separator, show_length); } +char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { + if (buffer_size == 0) { + return buffer; + } + // Calculate max bytes we can format: each byte needs 8 chars + size_t max_bytes = (buffer_size - 1) / 8; + if (max_bytes == 0 || length == 0) { + buffer[0] = '\0'; + return buffer; + } + size_t bytes_to_format = std::min(length, max_bytes); + + for (size_t byte_idx = 0; byte_idx < bytes_to_format; byte_idx++) { + for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) { + buffer[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0'; + } + } + buffer[bytes_to_format * 8] = '\0'; + return buffer; +} + std::string format_bin(const uint8_t *data, size_t length) { std::string result; result.resize(length * 8); - for (size_t byte_idx = 0; byte_idx < length; byte_idx++) { - for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) { - result[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0'; - } - } - + format_bin_to(&result[0], length * 8 + 1, data, length); return result; } @@ -487,19 +503,26 @@ static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; -// Helper function to find the index of a base64 character in the lookup table. +// Helper function to find the index of a base64/base64url character in the lookup table. // Returns the character's position (0-63) if found, or 0 if not found. +// Supports both standard base64 (+/) and base64url (-_) alphabets. // NOTE: This returns 0 for both 'A' (valid base64 char at index 0) and invalid characters. // This is safe because is_base64() is ALWAYS checked before calling this function, // preventing invalid characters from ever reaching here. The base64_decode function // stops processing at the first invalid character due to the is_base64() check in its // while loop condition, making this edge case harmless in practice. static inline uint8_t base64_find_char(char c) { + // Handle base64url variants: '-' maps to '+' (index 62), '_' maps to '/' (index 63) + if (c == '-') + return 62; + if (c == '_') + return 63; const char *pos = strchr(BASE64_CHARS, c); return pos ? (pos - BASE64_CHARS) : 0; } -static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); } +// Check if character is valid base64 or base64url +static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/') || (c == '-') || (c == '_')); } std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } @@ -617,6 +640,46 @@ std::vector base64_decode(const std::string &encoded_string) { return ret; } +/// Decode base64/base64url string directly into vector of little-endian int32 values +/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted) +/// @param out Output vector (cleared and filled with decoded int32 values) +/// @return true if successful, false if decode failed or invalid size +bool base64_decode_int32_vector(const std::string &base64, std::vector &out) { + // Decode in chunks to minimize stack usage + constexpr size_t chunk_bytes = 48; // 12 int32 values + constexpr size_t chunk_chars = 64; // 48 * 4/3 = 64 chars + uint8_t chunk[chunk_bytes]; + + out.clear(); + + const uint8_t *input = reinterpret_cast(base64.data()); + size_t remaining = base64.size(); + size_t pos = 0; + + while (remaining > 0) { + size_t chars_to_decode = std::min(remaining, chunk_chars); + size_t decoded_len = base64_decode(input + pos, chars_to_decode, chunk, chunk_bytes); + + if (decoded_len == 0) + return false; + + // Parse little-endian int32 values + for (size_t i = 0; i + 3 < decoded_len; i += 4) { + int32_t timing = static_cast(encode_uint32(chunk[i + 3], chunk[i + 2], chunk[i + 1], chunk[i])); + out.push_back(timing); + } + + // Check for incomplete int32 in last chunk + if (remaining <= chunk_chars && (decoded_len % 4) != 0) + return false; + + pos += chars_to_decode; + remaining -= chars_to_decode; + } + + return !out.empty(); +} + // Colors float gamma_correct(float value, float gamma) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 2e9c0e6b13..409c691cb1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1,8 +1,11 @@ #pragma once +#include #include #include +#include #include +#include #include #include #include @@ -18,6 +21,7 @@ #ifdef USE_ESP8266 #include +#include #endif #ifdef USE_RP2040 @@ -568,6 +572,53 @@ std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, /// sprintf-like function returning std::string. std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); +#ifdef USE_ESP8266 +// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM) +// Format strings must be wrapped with PSTR() macro +/// Safely append formatted string to buffer, returning new position (capped at size). +/// @param buf Output buffer +/// @param size Total buffer size +/// @param pos Current position in buffer +/// @param fmt Format string (must be in PROGMEM on ESP8266) +/// @return New position after appending (capped at size on overflow) +inline size_t buf_append_printf_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) { + if (pos >= size) { + return size; + } + va_list args; + va_start(args, fmt); + int written = vsnprintf_P(buf + pos, size - pos, fmt, args); + va_end(args); + if (written < 0) { + return pos; // encoding error + } + return std::min(pos + static_cast(written), size); +} +#define buf_append_printf(buf, size, pos, fmt, ...) buf_append_printf_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__) +#else +/// Safely append formatted string to buffer, returning new position (capped at size). +/// Handles snprintf edge cases: negative returns (encoding errors) and truncation. +/// @param buf Output buffer +/// @param size Total buffer size +/// @param pos Current position in buffer +/// @param fmt printf-style format string +/// @return New position after appending (capped at size on overflow) +__attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf, size_t size, size_t pos, + const char *fmt, ...) { + if (pos >= size) { + return size; + } + va_list args; + va_start(args, fmt); + int written = vsnprintf(buf + pos, size - pos, fmt, args); + va_end(args); + if (written < 0) { + return pos; // encoding error + } + return std::min(pos + static_cast(written), size); +} +#endif + /// Concatenate a name with a separator and suffix using an efficient stack-based approach. /// This avoids multiple heap allocations during string construction. /// Maximum name length supported is 120 characters for friendly names. @@ -1045,9 +1096,66 @@ std::string format_hex_pretty(T val, char separator = '.', bool show_length = tr return format_hex_pretty(reinterpret_cast(&val), sizeof(T), separator, show_length); } +/// Calculate buffer size needed for format_bin_to: "01234567...\0" = bytes * 8 + 1 +constexpr size_t format_bin_size(size_t byte_count) { return byte_count * 8 + 1; } + +/** Format byte array as binary string to buffer. + * + * Each byte is formatted as 8 binary digits (MSB first). + * Truncates output if data exceeds buffer capacity. + * + * @param buffer Output buffer to write to. + * @param buffer_size Size of the output buffer. + * @param data Pointer to the byte array to format. + * @param length Number of bytes in the array. + * @return Pointer to buffer. + * + * Buffer size needed: length * 8 + 1 (use format_bin_size()). + * + * Example: + * @code + * char buf[9]; // format_bin_size(1) + * format_bin_to(buf, sizeof(buf), data, 1); // "10101011" + * @endcode + */ +char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length); + +/// Format byte array as binary to buffer. Automatically deduces buffer size. +template inline char *format_bin_to(char (&buffer)[N], const uint8_t *data, size_t length) { + static_assert(N >= 9, "Buffer must hold at least one binary byte (9 chars)"); + return format_bin_to(buffer, N, data, length); +} + +/** Format an unsigned integer in binary to buffer, MSB first. + * + * @tparam N Buffer size (must be >= sizeof(T) * 8 + 1). + * @tparam T Unsigned integer type. + * @param buffer Output buffer to write to. + * @param val The unsigned integer value to format. + * @return Pointer to buffer. + * + * Example: + * @code + * char buf[9]; // format_bin_size(sizeof(uint8_t)) + * format_bin_to(buf, uint8_t{0xAA}); // "10101010" + * char buf16[17]; // format_bin_size(sizeof(uint16_t)) + * format_bin_to(buf16, uint16_t{0x1234}); // "0001001000110100" + * @endcode + */ +template::value, int> = 0> +inline char *format_bin_to(char (&buffer)[N], T val) { + static_assert(N >= sizeof(T) * 8 + 1, "Buffer too small for type"); + val = convert_big_endian(val); + return format_bin_to(buffer, reinterpret_cast(&val), sizeof(T)); +} + /// Format the byte array \p data of length \p len in binary. +/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. +/// Causes heap fragmentation on long-running devices. std::string format_bin(const uint8_t *data, size_t length); /// Format an unsigned integer in binary, starting with the most significant byte. +/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. +/// Causes heap fragmentation on long-running devices. template::value, int> = 0> std::string format_bin(T val) { val = convert_big_endian(val); return format_bin(reinterpret_cast(&val), sizeof(T)); @@ -1086,6 +1194,12 @@ std::vector base64_decode(const std::string &encoded_string); size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); +/// Decode base64/base64url string directly into vector of little-endian int32 values +/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted) +/// @param out Output vector (cleared and filled with decoded int32 values) +/// @return true if successful, false if decode failed or invalid size +bool base64_decode_int32_vector(const std::string &base64, std::vector &out); + ///@} /// @name Colors diff --git a/esphome/espota2.py b/esphome/espota2.py index 6349ad0fa8..95dd602ad2 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -400,6 +400,8 @@ def run_ota_impl_( "Error resolving IP address of %s. Is it connected to WiFi?", remote_host, ) + if not CORE.dashboard: + _LOGGER.error("(If you know the IP, try --device )") _LOGGER.error( "(If this error persists, please set a static IP address: " "https://esphome.io/components/wifi/#manual-ips)" diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 045b3f9168..5903e68e8e 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -28,8 +28,8 @@ dependencies: rules: - if: "target in [esp32s2, esp32s3, esp32p4]" esphome/esp-hub75: - version: 0.2.2 + version: 0.3.0 rules: - - if: "target in [esp32, esp32s2, esp32s3, esp32p4]" + - if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]" esp32async/asynctcp: version: 3.4.91 diff --git a/requirements_test.txt b/requirements_test.txt index 092a06fd66..d93a5d108f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.11 # also change in .pre-commit-config.yaml when updating +ruff==0.14.13 # 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/ci-custom.py b/script/ci-custom.py index e63e61e096..e227ec873e 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -682,6 +682,7 @@ def lint_trailing_whitespace(fname, match): # Heap-allocating helpers that cause fragmentation on long-running embedded devices. # These return std::string and should be replaced with stack-based alternatives. HEAP_ALLOCATING_HELPERS = { + "format_bin": "format_bin_to() with a stack buffer", "format_hex": "format_hex_to() with a stack buffer", "format_hex_pretty": "format_hex_pretty_to() with a stack buffer", "format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer", @@ -699,6 +700,7 @@ HEAP_ALLOCATING_HELPERS = { # get_mac_address(?!_) ensures we don't match get_mac_address_into_buffer, etc. # CPP_RE_EOL captures rest of line so NOLINT comments are detected r"[^\w](" + r"format_bin(?!_)|" r"format_hex(?!_)|" r"format_hex_pretty(?!_)|" r"format_mac_address_pretty|" diff --git a/script/determine-jobs.py b/script/determine-jobs.py index a61c9bf08d..318ac04a7d 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -90,7 +90,10 @@ class Platform(StrEnum): ESP32_S2_IDF = "esp32-s2-idf" ESP32_S3_IDF = "esp32-s3-idf" BK72XX_ARD = "bk72xx-ard" # LibreTiny BK7231N + RTL87XX_ARD = "rtl87xx-ard" # LibreTiny RTL8720x + LN882X_ARD = "ln882x-ard" # LibreTiny LN882x RP2040_ARD = "rp2040-ard" # Raspberry Pi Pico + NRF52_ZEPHYR = "nrf52-adafruit" # Nordic nRF52 (Zephyr) # Memory impact analysis constants @@ -110,7 +113,7 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset( "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 + "nrf52", # Nordic nRF52 platform implementation (uses Zephyr) } ) @@ -122,8 +125,9 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset( # fastest build times, most sensitive to code size changes # 3. ESP32 IDF - Primary ESP32 platform, most representative of modern ESPHome # 4-6. Other ESP32 variants - Less commonly used but still supported -# 7. BK72XX - LibreTiny platform (good for detecting LibreTiny-specific changes) -# 8. RP2040 - Raspberry Pi Pico platform +# 7-9. LibreTiny platforms (BK72XX, RTL87XX, LN882X) - good for detecting LibreTiny-specific changes +# 10. RP2040 - Raspberry Pi Pico platform +# 11. nRF52 - Nordic nRF52 with Zephyr (good for detecting Zephyr-specific changes) MEMORY_IMPACT_PLATFORM_PREFERENCE = [ Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee) Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained, fastest builds) @@ -132,7 +136,10 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [ Platform.ESP32_S2_IDF, # ESP32-S2 IDF Platform.ESP32_S3_IDF, # ESP32-S3 IDF Platform.BK72XX_ARD, # LibreTiny BK7231N + Platform.RTL87XX_ARD, # LibreTiny RTL8720x + Platform.LN882X_ARD, # LibreTiny LN882x Platform.RP2040_ARD, # Raspberry Pi Pico + Platform.NRF52_ZEPHYR, # Nordic nRF52 (Zephyr) ] @@ -411,6 +418,8 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None: - wifi_component_esp8266.cpp, *_esp8266.h -> ESP8266_ARD - *_esp32*.cpp -> ESP32 IDF (generic) - *_libretiny.cpp, *_bk72*.* -> BK72XX (LibreTiny) + - *_rtl87*.* -> RTL87XX (LibreTiny Realtek) + - *_ln882*.* -> LN882X (LibreTiny Lightning) - *_pico.cpp, *_rp2040.* -> RP2040_ARD Args: @@ -444,7 +453,12 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None: if "esp32" in filename_lower: return Platform.ESP32_IDF - # LibreTiny (via 'libretiny' pattern or BK72xx-specific files) + # LibreTiny platforms (check specific variants before generic libretiny) + # Check specific variants first to handle paths like libretiny/wifi_rtl87xx.cpp + if "rtl87" in filename_lower: + return Platform.RTL87XX_ARD + if "ln882" in filename_lower: + return Platform.LN882X_ARD if "libretiny" in filename_lower or "bk72" in filename_lower: return Platform.BK72XX_ARD @@ -452,6 +466,10 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None: if "pico" in filename_lower or "rp2040" in filename_lower: return Platform.RP2040_ARD + # nRF52 / Zephyr + if "nrf52" in filename_lower or "zephyr" in filename_lower: + return Platform.NRF52_ZEPHYR + return None diff --git a/tests/component_tests/image/config/mm_dimensions.svg b/tests/component_tests/image/config/mm_dimensions.svg new file mode 100644 index 0000000000..bb64433a4d --- /dev/null +++ b/tests/component_tests/image/config/mm_dimensions.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index 930bbac8d1..c9481a0e1d 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -5,17 +5,21 @@ from __future__ import annotations from collections.abc import Callable from pathlib import Path from typing import Any +from unittest.mock import MagicMock, patch import pytest from esphome import config_validation as cv from esphome.components.image import ( + CONF_INVERT_ALPHA, + CONF_OPAQUE, CONF_TRANSPARENCY, CONFIG_SCHEMA, get_all_image_metadata, get_image_metadata, + write_image, ) -from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE +from esphome.const import CONF_DITHER, CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE from esphome.core import CORE @@ -350,3 +354,52 @@ def test_get_all_image_metadata_empty() -> None: "get_all_image_metadata should always return a dict" ) # Length could be 0 or more depending on what's in CORE at test time + + +@pytest.fixture +def mock_progmem_array(): + """Mock progmem_array to avoid needing a proper ID object in tests.""" + with patch("esphome.components.image.cg.progmem_array") as mock_progmem: + mock_progmem.return_value = MagicMock() + yield mock_progmem + + +@pytest.mark.asyncio +async def test_svg_with_mm_dimensions_succeeds( + component_config_path: Callable[[str], Path], + mock_progmem_array: MagicMock, +) -> None: + """Test that SVG files with dimensions in mm are successfully processed.""" + # Create a config for write_image without CONF_RESIZE + config = { + CONF_FILE: component_config_path("mm_dimensions.svg"), + CONF_TYPE: "BINARY", + CONF_TRANSPARENCY: CONF_OPAQUE, + CONF_DITHER: "NONE", + CONF_INVERT_ALPHA: False, + CONF_RAW_DATA_ID: "test_raw_data_id", + } + + # This should succeed without raising an error + result = await write_image(config) + + # Verify that write_image returns the expected tuple + assert isinstance(result, tuple), "write_image should return a tuple" + assert len(result) == 6, "write_image should return 6 values" + + prog_arr, width, height, image_type, trans_value, frame_count = result + + # Verify the dimensions are positive integers + # At 100 DPI, 10mm = ~39 pixels (10mm * 100dpi / 25.4mm_per_inch) + assert isinstance(width, int), "Width should be an integer" + assert isinstance(height, int), "Height should be an integer" + assert width > 0, "Width should be positive" + assert height > 0, "Height should be positive" + assert frame_count == 1, "Single image should have frame_count of 1" + # Verify we got reasonable dimensions from the mm-based SVG + assert 30 < width < 50, ( + f"Width should be around 39 pixels for 10mm at 100dpi, got {width}" + ) + assert 30 < height < 50, ( + f"Height should be around 39 pixels for 10mm at 100dpi, got {height}" + ) diff --git a/tests/components/debug/common.yaml b/tests/components/debug/common.yaml index d9a61f8df0..59ba39c3a4 100644 --- a/tests/components/debug/common.yaml +++ b/tests/components/debug/common.yaml @@ -11,6 +11,8 @@ sensor: - platform: debug free: name: "Heap Free" + block: + name: "Heap Block" loop_time: name: "Loop Time" cpu_frequency: diff --git a/tests/components/debug/test.bk72xx-ard.yaml b/tests/components/debug/test.bk72xx-ard.yaml index dade44d145..fdae374788 100644 --- a/tests/components/debug/test.bk72xx-ard.yaml +++ b/tests/components/debug/test.bk72xx-ard.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +sensor: + - platform: debug + min_free: + name: "Heap Min Free" diff --git a/tests/components/debug/test.esp32-ard.yaml b/tests/components/debug/test.esp32-ard.yaml index 8e19a4d627..8f93b0925e 100644 --- a/tests/components/debug/test.esp32-ard.yaml +++ b/tests/components/debug/test.esp32-ard.yaml @@ -2,3 +2,10 @@ esp32: cpu_frequency: 240MHz + +sensor: + - platform: debug + fragmentation: + name: "Heap Fragmentation" + min_free: + name: "Heap Min Free" diff --git a/tests/components/debug/test.esp32-idf.yaml b/tests/components/debug/test.esp32-idf.yaml index f7483a54b3..6a9996ad06 100644 --- a/tests/components/debug/test.esp32-idf.yaml +++ b/tests/components/debug/test.esp32-idf.yaml @@ -9,5 +9,9 @@ sensor: name: "Heap Free" psram: name: "Free PSRAM" + fragmentation: + name: "Heap Fragmentation" + min_free: + name: "Heap Min Free" psram: diff --git a/tests/components/debug/test.esp32-s2-idf.yaml b/tests/components/debug/test.esp32-s2-idf.yaml index dade44d145..80919b0bab 100644 --- a/tests/components/debug/test.esp32-s2-idf.yaml +++ b/tests/components/debug/test.esp32-s2-idf.yaml @@ -1 +1,8 @@ <<: !include common.yaml + +sensor: + - platform: debug + fragmentation: + name: "Heap Fragmentation" + min_free: + name: "Heap Min Free" diff --git a/tests/components/debug/test.esp8266-ard.yaml b/tests/components/debug/test.esp8266-ard.yaml index dade44d145..1398087bf0 100644 --- a/tests/components/debug/test.esp8266-ard.yaml +++ b/tests/components/debug/test.esp8266-ard.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +sensor: + - platform: debug + fragmentation: + name: "Heap Fragmentation" diff --git a/tests/components/debug/test.ln882x-ard.yaml b/tests/components/debug/test.ln882x-ard.yaml index dade44d145..fdae374788 100644 --- a/tests/components/debug/test.ln882x-ard.yaml +++ b/tests/components/debug/test.ln882x-ard.yaml @@ -1 +1,6 @@ <<: !include common.yaml + +sensor: + - platform: debug + min_free: + name: "Heap Min Free" diff --git a/tests/components/debug/test.rtl87xx-ard.yaml b/tests/components/debug/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..fdae374788 --- /dev/null +++ b/tests/components/debug/test.rtl87xx-ard.yaml @@ -0,0 +1,6 @@ +<<: !include common.yaml + +sensor: + - platform: debug + min_free: + name: "Heap Min Free" diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 134ad4d046..3b888c3d19 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -53,6 +53,17 @@ binary_sensor: // Garage Door is closed. return false; } + - platform: template + id: select_binary_sensor + name: Select is one or two + condition: + any: + - select.is: + id: template_select + options: [one, two] + - select.is: + id: template_select + lambda: return current == id(template_text).state; - platform: template id: other_binary_sensor name: "Garage Door Closed" @@ -320,6 +331,7 @@ valve: text: - platform: template + id: template_text name: "Template text" optimistic: true min_length: 0 diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index bd20cb3e21..61ef8985df 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1472,6 +1472,24 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> determine_jobs.Platform.BK72XX_ARD, ), ("esphome/components/ble/ble_bk72xx.cpp", determine_jobs.Platform.BK72XX_ARD), + # RTL87xx (LibreTiny Realtek) detection + ( + "tests/components/logger/test.rtl87xx-ard.yaml", + determine_jobs.Platform.RTL87XX_ARD, + ), + ( + "esphome/components/libretiny/wifi_rtl87xx.cpp", + determine_jobs.Platform.RTL87XX_ARD, + ), + # LN882x (LibreTiny Lightning) detection + ( + "tests/components/logger/test.ln882x-ard.yaml", + determine_jobs.Platform.LN882X_ARD, + ), + ( + "esphome/components/libretiny/wifi_ln882x.cpp", + determine_jobs.Platform.LN882X_ARD, + ), # RP2040 / Raspberry Pi Pico detection ("esphome/components/gpio/gpio_rp2040.cpp", determine_jobs.Platform.RP2040_ARD), ("esphome/components/wifi/wifi_rp2040.cpp", determine_jobs.Platform.RP2040_ARD), @@ -1481,6 +1499,23 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> "tests/components/rp2040/test.rp2040-ard.yaml", determine_jobs.Platform.RP2040_ARD, ), + # nRF52 / Zephyr detection + ( + "tests/components/logger/test.nrf52-adafruit.yaml", + determine_jobs.Platform.NRF52_ZEPHYR, + ), + ( + "esphome/components/nrf52/gpio.cpp", + determine_jobs.Platform.NRF52_ZEPHYR, + ), + ( + "esphome/components/zephyr/core.cpp", + determine_jobs.Platform.NRF52_ZEPHYR, + ), + ( + "esphome/components/zephyr_ble_server/ble_server.cpp", + determine_jobs.Platform.NRF52_ZEPHYR, + ), # No platform hint (generic files) ("esphome/components/wifi/wifi.cpp", None), ("esphome/components/sensor/sensor.h", None), @@ -1501,11 +1536,19 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> "esp32_in_name", "libretiny", "bk72xx", + "rtl87xx_test_yaml", + "rtl87xx_wifi", + "ln882x_test_yaml", + "ln882x_wifi", "rp2040_gpio", "rp2040_wifi", "pico_i2c", "pico_spi", "rp2040_test_yaml", + "nrf52_test_yaml", + "nrf52_gpio", + "zephyr_core", + "zephyr_ble_server", "generic_wifi_no_hint", "generic_sensor_no_hint", "core_helpers_no_hint", @@ -1532,6 +1575,11 @@ def test_detect_platform_hint_from_filename( ("file_ESP8266.cpp", determine_jobs.Platform.ESP8266_ARD), # ESP32 with different cases ("file_ESP32.cpp", determine_jobs.Platform.ESP32_IDF), + # nRF52/Zephyr with different cases + ("file_NRF52.cpp", determine_jobs.Platform.NRF52_ZEPHYR), + ("file_Nrf52.cpp", determine_jobs.Platform.NRF52_ZEPHYR), + ("file_ZEPHYR.cpp", determine_jobs.Platform.NRF52_ZEPHYR), + ("file_Zephyr.cpp", determine_jobs.Platform.NRF52_ZEPHYR), ], ids=[ "rp2040_uppercase", @@ -1540,6 +1588,10 @@ def test_detect_platform_hint_from_filename( "pico_titlecase", "esp8266_uppercase", "esp32_uppercase", + "nrf52_uppercase", + "nrf52_mixedcase", + "zephyr_uppercase", + "zephyr_titlecase", ], ) def test_detect_platform_hint_from_filename_case_insensitive(